@shadow-js/realtime 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +35 -0
- package/dist/dev/client/core.js +104 -106
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/sse.js +33 -24
- package/dist/dev/client/sse.js.map +1 -1
- package/dist/dev/client/types.d.ts +6 -0
- package/dist/dev/client/util.js +2 -2
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/cloudflare/link.js +69 -8
- package/dist/dev/cloudflare/link.js.map +1 -1
- package/dist/dev/memory.d.ts +11 -0
- package/dist/dev/memory.js +38 -10
- package/dist/dev/memory.js.map +1 -1
- package/dist/dev/protocol.js +13 -12
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/redis/topics.d.ts +6 -1
- package/dist/dev/redis/topics.js +4 -1
- package/dist/dev/redis/topics.js.map +1 -1
- package/dist/dev/server/http.js +37 -24
- package/dist/dev/server/http.js.map +1 -1
- package/dist/dev/server/hub.js +66 -39
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +2 -2
- package/dist/dev/server/index.js.map +1 -1
- package/dist/dev/server/limits.d.ts +4 -1
- package/dist/dev/server/limits.js.map +1 -1
- package/dist/dev/server/rules.js.map +1 -1
- package/dist/prod/client/core.js +1 -1
- package/dist/prod/client/core.js.map +1 -1
- package/dist/prod/client/sse.js +1 -4
- package/dist/prod/client/sse.js.map +1 -1
- package/dist/prod/client/util.js +1 -1
- package/dist/prod/client/util.js.map +1 -1
- package/dist/prod/cloudflare/link.js +1 -1
- package/dist/prod/cloudflare/link.js.map +1 -1
- package/dist/prod/memory.js +1 -1
- package/dist/prod/memory.js.map +1 -1
- package/dist/prod/protocol.js +1 -1
- package/dist/prod/protocol.js.map +1 -1
- package/dist/prod/redis/topics.js +1 -1
- package/dist/prod/redis/topics.js.map +1 -1
- package/dist/prod/server/http.js +1 -1
- package/dist/prod/server/http.js.map +1 -1
- package/dist/prod/server/hub.js +1 -1
- package/dist/prod/server/hub.js.map +1 -1
- package/dist/prod/server/index.js.map +1 -1
- package/dist/prod/server/limits.js +1 -1
- package/dist/prod/server/limits.js.map +1 -1
- package/dist/prod/server/rules.js +1 -1
- package/dist/prod/server/rules.js.map +1 -1
- package/package.json +6 -5
package/dist/prod/server/hub.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{RealtimeError as e}from"../errors.js";import{byteLength as t,decodeClientFrame as n,isChannelName as r,isEventName as i,isIdempotencyKey as a}from"../protocol.js";import{TokenBucket as o,WindowCounter as s,resolveLimits as c}from"./limits.js";import{memoryAdapter as l}from"../memory.js";import{compileRule as u,matchRule as d}from"./rules.js";function f(){let e=/* @__PURE__ */ new Uint8Array(16);crypto.getRandomValues(e);let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function p(e){return JSON.stringify(e)}function m(e,t,n){return`{"t":"msg","c":${JSON.stringify(e)},"s":${t.seq},"e":${JSON.stringify(t.event)},"d":${t.data}`+(t.publisher===void 0?``:`,"p":${JSON.stringify(t.publisher)}`)+(t.user===void 0?``:`,"u":${JSON.stringify(t.user)}`)+(n?`,"r":1}`:`}`)}function h(h={}){let g=c(h.limits),_=h.now??Date.now,v=h.adapter===void 0,y=h.adapter??l({now:_}),b=[],x=/* @__PURE__ */ new Map,S=/* @__PURE__ */ new Map,C,w=!1,T=!1,E={published:0,delivered:0,refused:0,slowConsumers:0},D=(e,t)=>{if(h.onError!==void 0)try{h.onError(e,t)}catch{}else console.error(`[realtime] ${t.where}`,e)};function O(e){for(let t of b){let n=d(t,e);if(n!==void 0)return{rule:t.rule,params:n}}}async function k(t,n,r,i,a){let o=t[n];if(o===void 0||o===!1)return!1;if(o===!0)return!0;try{return await o({channel:r,params:i,identity:a,op:n})===!0}catch(t){throw D(t,{where:`rule.${n}`,channel:r}),new e(`unavailable`,`the channel rule could not be evaluated`)}}function A(e,t){if(!e.closed){try{if((e.transport.buffered?.()??0)>g.maxBufferedBytes){E.slowConsumers+=1,N(e,1013,`slow consumer`);return}if(e.transport.send(t))return}catch(t){D(t,{where:`transport.send`,connection:e.id})}Z(e)}}function j(e,t){A(e,p(t))}function M(e,t,n,r,i){E.refused+=1,j(e,{t:`err`,...t===void 0?{}:{n:t},...r===void 0?{}:{c:r},code:n,...i===void 0?{}:{msg:i}})}function N(e,t,n){if(!e.closed){try{e.transport.send(p({t:`bye`,code:t,why:n}))}catch{}try{e.transport.close(t,n)}catch{}Z(e)}}function P(e){let t=S.get(e);return(t===void 0||t.closing)&&(t={name:e,subs:/* @__PURE__ */ new Set,bucket:new o(g.channelPublishesPerSecond,g.channelPublishesPerSecond,_()),adapterSub:void 0,presenceSub:void 0,closing:!1,frame:void 0},S.set(e,t)),t}async function F(e){e.adapterSub===void 0&&(e.adapterSub=y.subscribe(e.name,t=>L(e,t)),e.adapterSub.catch(()=>{e.adapterSub=void 0})),await e.adapterSub}async function I(e){e.presenceSub===void 0&&(e.presenceSub=y.presenceSubscribe(e.name,t=>B(e,t)),e.presenceSub.catch(()=>{e.presenceSub=void 0})),await e.presenceSub}function L(e,t){for(let n of e.subs)n.active&&(n.pumping?n.buffer.length>=1e4?H(n,`gap`):n.buffer.push(t):t.seq===n.lastSeq+1?R(n,t,!1):t.seq>n.lastSeq+1?(n.buffer.push(t),z(n,void 0)):t.at>n.lastAt&&H(n,`reset`))}function R(e,t,n){if(e.lastSeq=t.seq,e.lastAt=Math.max(e.lastAt,t.at),!e.echo&&t.publisher===e.conn.id)return;if(E.delivered+=1,n){A(e.conn,m(e.channel.name,t,!0));return}let r=e.channel.frame,i=r!==void 0&&r.seq===t.seq?r.text:m(e.channel.name,t,!1);(r===void 0||r.seq!==t.seq)&&(e.channel.frame={seq:t.seq,text:i}),A(e.conn,i)}async function z(e,t){if(!(e.pumping&&t===void 0)){e.pumping=!0;try{for(;e.active;){e.buffer=e.buffer.filter(t=>t.seq>e.lastSeq);let n=e.buffer[0];if(n!==void 0&&n.seq===e.lastSeq+1){R(e,n,!1),e.buffer.shift();continue}let r=n===void 0?t??0:n.seq-1;if(e.lastSeq>=r)break;let i=await y.history(e.channel.name,e.lastSeq,Math.min(500,r-e.lastSeq));if(!e.active)return;let a=i.messages[0];if(a===void 0||a.seq!==e.lastSeq+1){if(!e.gapOk){H(e,`gap`);return}e.lastSeq=(a?.seq??r+1)-1}else for(let t of i.messages){if(t.seq>r)break;R(e,t,!0)}}}catch(t){D(t,{where:`pump`,channel:e.channel.name,connection:e.conn.id}),e.active&&H(e,`unavailable`)}finally{e.pumping=!1}e.active&&e.buffer.length>0&&z(e,void 0)}}function B(e,t){for(let n of e.subs)n.active&&(n.joined||n.watch)&&(n.presenceQueue===void 0?V(n,t):n.presenceQueue.push(t))}function V(e,t){j(e.conn,{t:`pres`,c:t.channel,op:t.op,m:t.member})}function H(e,t){e.active&&(e.active=!1,e.conn.subs.delete(e.channel.name),e.channel.subs.delete(e),t!==void 0&&!e.conn.closed&&j(e.conn,{t:`unsubbed`,c:e.channel.name,...t===``?{}:{why:t}}),W(e))}let U=/* @__PURE__ */ new Set;function W(e){let t=G(e);return U.add(t),t.finally(()=>U.delete(t)),t}async function G(e){let t=e.channel;try{await e.joining?.catch(()=>void 0),e.joined&&(e.joined=!1,await y.presenceRemove(t.name,e.conn.id))}catch(n){D(n,{where:`presenceRemove`,channel:t.name,connection:e.conn.id})}if(t.subs.size>0||t.closing)return;t.closing=!0,S.get(t.name)===t&&S.delete(t.name);let n=[t.adapterSub,t.presenceSub].filter(e=>e!==void 0);await Promise.all(n.map(async e=>{try{await(await e)()}catch(e){D(e,{where:`unsubscribe`,channel:t.name})}}))}function K(e){return e.identity.user===void 0?`unauthorized`:`forbidden`}async function q(n,r){let{c:i,n:a}=r;if(n.subs.has(i))return M(n,a,`conflict`,i,`already subscribed`);if(n.subs.size>=g.maxChannelsPerConnection)return M(n,a,`limit`,i,`too many channels on this connection`);let o=O(i);if(o===void 0||!await k(o.rule,`subscribe`,i,o.params,n.identity))return M(n,a,K(n),i);let s=r.hasPresence||r.watch===!0;if(s&&!await k(o.rule,`presence`,i,o.params,n.identity))return M(n,a,K(n),i);if(r.hasPresence&&t(JSON.stringify(r.presence??null))>g.maxStateBytes)return M(n,a,`too_large`,i,`the presence state is too large`);if(n.closed)return;let c=P(i);if(c.subs.size>=g.maxSubscribersPerChannel)return M(n,a,`limit`,i,`the channel is full`);let l={conn:n,channel:c,rule:o.rule,params:o.params,echo:r.echo!==!1,watch:r.watch===!0,joined:!1,version:0,lastSeq:0,lastAt:_(),pumping:!0,gapOk:!1,buffer:[],presenceQueue:s?[]:void 0,joining:void 0,active:!0};n.subs.set(i,l),c.subs.add(l);try{await F(c),s&&(await I(c),Q());let e=await y.history(i,2**53-1,0);if(!l.active)return;let t=e.head,o=r.after,u=!1,d=!1;if(o===void 0?l.lastSeq=t:o>t?(d=!0,l.lastSeq=t):(l.lastSeq=o,u=e.oldest===0?t>o:e.oldest>o+1,l.gapOk=u),j(n,{t:`subbed`,n:a,c:i,head:t,...u?{gap:!0}:{},...d?{reset:!0}:{}}),r.hasPresence&&(l.joining=J(l,r.presence??null),await l.joining),s&&await ee(l),!l.active)return;await z(l,t),l.gapOk=!1}catch(t){l.active&&(H(l),D(t,{where:`subscribe`,channel:i,connection:n.id}),M(n,a,t instanceof e?t.code:`unavailable`,i))}}async function J(t,n){if((await y.presenceList(t.channel.name)).length>=g.maxPresenceMembers)throw new e(`limit`,`the presence set is full`);t.version=1,t.joined=!0,Q();let r={id:t.conn.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:n,v:1};await y.presenceSet(t.channel.name,r,g.presenceTtlMs)}async function ee(e){let t=await y.presenceList(e.channel.name);if(!e.active)return;j(e.conn,{t:`pres`,c:e.channel.name,op:`sync`,m:t});let n=e.presenceQueue??[];e.presenceQueue=void 0;let r=new Map(t.map(e=>[e.id,e.v]));for(let t of n){let n=r.get(t.member.id);(t.op===`leave`?n===void 0:n!==void 0&&n>=t.member.v)||V(e,t)}}async function te(e,n){let r=e.subs.get(n.c);if(r===void 0||!r.joined)return M(e,void 0,`not_subscribed`,n.c);if(t(JSON.stringify(n.s))>g.maxStateBytes)return M(e,void 0,`too_large`,n.c,`the presence state is too large`);if(!e.publishBucket.take(_()))return Y(e,void 0,n.c);r.version+=1;let i={id:e.id,...e.identity.user===void 0?{}:{u:e.identity.user},s:n.s,v:r.version};try{await y.presenceSet(n.c,i,g.presenceTtlMs)}catch(t){D(t,{where:`presenceSet`,channel:n.c,connection:e.id}),M(e,void 0,`unavailable`,n.c)}}function Y(e,t,n){M(e,t,`rate_limited`,n),e.limited.hit(_())&&N(e,1008,`rate limit exceeded`)}async function ne(e,t){let{c:n,n:r}=t,i=O(n);if(i===void 0||!await k(i.rule,`publish`,n,i.params,e.identity))return M(e,r,K(e),n);if(t.d.length>g.maxMessageBytes)return M(e,r,`too_large`,n,`the message is too large`);if(!e.publishBucket.take(_())||S.get(n)?.bucket.take(_())===!1)return Y(e,r,n);try{let a=await y.publish(n,{event:t.e,data:t.d,publisher:e.id,...e.identity.user===void 0?{}:{user:e.identity.user}},{history:i.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...t.k===void 0?{}:{key:t.k}});E.published+=+!a.duplicate,r!==void 0&&j(e,{t:`ack`,n:r,s:a.seq,...a.duplicate?{dup:!0}:{}})}catch(t){D(t,{where:`publish`,channel:n,connection:e.id}),M(e,r,`unavailable`,n)}}async function re(n,r){let{c:i,n:a}=r,o=n.subs.get(i);if(o===void 0&&n.stateless){let e=O(i);e!==void 0&&await k(e.rule,`subscribe`,i,e.params,n.identity)&&(o=e)}if(o===void 0)return M(n,a,K(n)===`unauthorized`&&n.stateless?`unauthorized`:`not_subscribed`,i);let s=o,c=s.rule.handlers?.[r.e];if(c===void 0)return M(n,a,`no_handler`,i);if(n.pending.size>=g.maxPendingRequests)return M(n,a,`limit`,i,`too many requests in flight`);if(!n.publishBucket.take(_()))return Y(n,a,i);let l=new AbortController;n.pending.add(l);let u=Math.min(r.ms??g.requestTimeoutMs,g.requestTimeoutMs),d=setTimeout(()=>l.abort(new e(`timeout`)),u);(async()=>{try{let e=Promise.resolve(c({channel:i,params:s.params,identity:n.identity,event:r.e,data:r.d,signal:l.signal})),o=new Promise((e,t)=>{l.signal.addEventListener(`abort`,()=>t(l.signal.reason),{once:!0})}),u=await Promise.race([e,o])??null,d=JSON.stringify(u);if(d===void 0||t(d)>g.maxMessageBytes)return M(n,a,`too_large`,i,`the reply is too large`);A(n,`{"t":"rep","n":${a},"d":${d}}`)}catch(t){t instanceof e?M(n,a,t.code,i,t.code===`timeout`||t.code===`closed`?void 0:t.message):(D(t,{where:`request`,channel:i,connection:n.id}),M(n,a,`handler_error`,i))}finally{clearTimeout(d),n.pending.delete(l),X(n)}})()}async function ie(t,r){let i=n(r,g.maxMessageBytes);if(!i.ok){M(t,i.n,i.code,void 0,i.why),t.invalid.hit(_())&&N(t,1008,`too many invalid frames`);return}let a=i.frame;try{switch(a.t){case`hello`:return;case`ping`:j(t,{t:`pong`,...a.n===void 0?{}:{n:a.n}});return;case`sub`:return await q(t,a);case`unsub`:{let e=t.subs.get(a.c);if(e===void 0)return M(t,a.n,`not_subscribed`,a.c);H(e);return}case`pub`:return await ne(t,a);case`req`:return await re(t,a);case`pres`:return await te(t,a)}}catch(n){let r=`n`in a?a.n:void 0,i=`c`in a?a.c:void 0;n instanceof e?M(t,r,n.code,i):(D(n,{where:`frame`,connection:t.id,...i===void 0?{}:{channel:i}}),M(t,r,`unavailable`,i))}}async function ae(t){if(!t.running){t.running=!0;try{for(let n=t.queue.shift();n!==void 0&&!t.closed;n=t.queue.shift())try{await ie(t,n)}catch(n){n instanceof e?M(t,void 0,n.code):(D(n,{where:`frame`,connection:t.id}),M(t,void 0,`unavailable`))}}finally{t.running=!1,X(t)}}}function X(e){if(!(e.running||e.queue.length>0||e.pending.size>0))for(let t of e.waiters.splice(0))t()}async function Z(t){if(t.closed)return t.done;t.closed=!0,x.get(t.id)===t&&x.delete(t.id);for(let n of t.pending)n.abort(new e(`closed`));t.pending.clear(),t.queue.length=0;for(let e of t.waiters.splice(0))e();let n=[...t.subs.values()];for(let e of n)e.active=!1,e.channel.subs.delete(e);t.subs.clear(),await Promise.allSettled(n.map(e=>W(e))),t.release?.()}function Q(){if(C!==void 0||w)return;let e=setInterval(()=>void oe(),g.presenceHeartbeatMs);e.unref?.(),C=e}async function oe(){if(!T){T=!0;try{await se()}finally{T=!1}}}async function se(){let e=[],t=/* @__PURE__ */ new Set;for(let n of x.values())for(let r of n.subs.values())r.joined&&e.push({channel:r.channel.name,id:n.id}),(r.joined||r.watch)&&t.add(r.channel.name);if(e.length===0&&t.size===0)clearInterval(C),C=void 0;else try{let n=await y.presenceTouch(e,g.presenceTtlMs);for(let e of n){let t=x.get(e.id)?.subs.get(e.channel);t!==void 0&&t.joined&&await y.presenceSet(e.channel,{id:e.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:null,v:t.version},g.presenceTtlMs)}for(let e of t)await y.presenceReap(e)}catch(e){D(e,{where:`presenceHeartbeat`})}}for(let[e,t]of Object.entries(h.channels??{}))b.push(u(e,t));let $={limits:g,adapter:y,report:D,match:O,allowed:k,channel(e,t){return b.push(u(e,t)),$},connect(t,n={},r={}){if(w)throw new e(`closed`,`the hub is closed`);let i=()=>void 0,a=new Promise(e=>{i=e}),c=r.actingFor===void 0?void 0:x.get(r.actingFor),l=c!==void 0&&n.user!==void 0&&c.identity.user===n.user,u={id:l?r.actingFor:f(),identity:n,transport:t,subs:/* @__PURE__ */ new Map,queue:[],publishBucket:r.publishBucket??new o(g.publishesPerSecond,g.publishBurst,_()),stateless:r.stateless===!0,waiters:[],invalid:new s(10,6e4),limited:new s(50,1e4),pending:/* @__PURE__ */ new Set,running:!1,closed:!1,release:i,done:a};return l||x.set(u.id,u),j(u,{t:`ready`,v:1,id:u.id,hb:g.heartbeatMs,max:g.maxMessageBytes}),{id:u.id,identity:n,done:a,receive(e){u.closed||(u.queue.length>=g.maxQueuedFrames?N(u,1013,`too many frames waiting`):(u.queue.push(e),ae(u)))},closed(){Z(u)},settled(){return new Promise(e=>{u.waiters.push(e),X(u)})}}},async publish(n,o,s,c={}){if(!r(n))throw new e(`bad_channel`,`"${n}" is not a valid channel name`);if(!i(o))throw new e(`bad_frame`,`"${o}" is not a valid event name`);if(c.key!==void 0&&!a(c.key))throw new e(`bad_frame`,`the idempotency key is not valid`);let l;try{l=JSON.stringify(s===void 0?null:s)}catch(e){throw TypeError(`the data is not JSON`,{cause:e})}if(l===void 0)throw TypeError(`the data is not JSON`);if(t(l)>g.maxMessageBytes)throw new e(`too_large`,`the message is too large`);let u=O(n),d=await y.publish(n,{event:o,data:l},{history:u?.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...c.key===void 0?{}:{key:c.key}});return E.published+=+!d.duplicate,d},async setMemberState(n,r,i,a){if(n.user===void 0)throw new e(`unauthorized`,`presence changes over HTTP need a signed-in user`);let o=O(r);if(o===void 0||!await k(o.rule,`presence`,r,o.params,n))throw new e(`forbidden`);if(t(JSON.stringify(a))>g.maxStateBytes)throw new e(`too_large`,`the presence state is too large`);let s=(await y.presenceList(r)).find(e=>e.id===i);if(s===void 0||s.u!==n.user)throw new e(`forbidden`,`that connection is not yours`);await y.presenceSet(r,{id:s.id,u:s.u,s:a,v:s.v+1},g.presenceTtlMs)},revoke(e){if(e.user===void 0&&e.connection===void 0&&e.channel===void 0)throw TypeError(`revoke needs a user, a connection or a channel.`);let t=0;for(let n of[...x.values()])if((e.connection===void 0||n.id===e.connection)&&(e.user===void 0||n.identity.user===e.user))for(let r of[...n.subs.values()])(e.channel===void 0||r.channel.name===e.channel)&&(H(r,`revoked`),t+=1);return t},stats(){let e=0;for(let t of x.values())e+=t.subs.size;return{connections:x.size,channels:S.size,subscriptions:e,...E}},async close(){if(w)return;w=!0,C!==void 0&&clearInterval(C),C=void 0;let e=[...x.values()];for(let t of e)N(t,1001,`going away`);await Promise.all(e.map(e=>e.done)),await Promise.allSettled([...U]),v&&await y.close()}};return $}export{h as createHubCore};
|
|
1
|
+
import{RealtimeError as e}from"../errors.js";import{byteLength as t,decodeClientFrame as n,isChannelName as r,isEventName as i,isIdempotencyKey as a}from"../protocol.js";import{TokenBucket as o,WindowCounter as s,resolveLimits as c}from"./limits.js";import{memoryAdapter as l}from"../memory.js";import{compileRule as u,matchRule as d}from"./rules.js";function f(){let e=crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));return btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function p(e){return JSON.stringify(e)}function m(e,t,n){return`{"t":"msg","c":${JSON.stringify(e)},"s":${t.seq},"e":${JSON.stringify(t.event)},"d":${t.data}`+(t.publisher===void 0?``:`,"p":${JSON.stringify(t.publisher)}`)+(t.user===void 0?``:`,"u":${JSON.stringify(t.user)}`)+(n?`,"r":1}`:`}`)}function h(h={}){let g=c(h.limits),_=h.now??Date.now,v=h.adapter===void 0,y=h.adapter??l({now:_,onError:(e,t)=>D(e,{where:`memory.${t.where}`,channel:t.channel})}),b=[],x=/* @__PURE__ */ new Map,S=/* @__PURE__ */ new Map,C,w=!1,T=!1,E={published:0,delivered:0,refused:0,slowConsumers:0},D=(e,t)=>{if(h.onError!==void 0)try{h.onError(e,t)}catch{}else console.error(`[realtime] ${t.where}`,e)};function O(e){for(let t of b){let n=d(t,e);if(n!==void 0)return{rule:t.rule,params:n}}}async function k(t,n,r,i,a){let o=t[n];if(o===void 0||o===!1)return!1;if(o===!0)return!0;try{return await o({channel:r,params:i,identity:a,op:n})===!0}catch(t){throw D(t,{where:`rule.${n}`,channel:r}),new e(`unavailable`,void 0)}}function A(e,t){if(!e.closed){try{if((e.transport.buffered?.()??0)>g.maxBufferedBytes){E.slowConsumers+=1,P(e,1013,``);return}if(e.transport.send(t))return}catch(t){D(t,{where:`transport.send`,connection:e.id}),j(e,`transport.close`,()=>e.transport.close(1011,``))}Z(e)}}function j(e,t,n){try{n()}catch(n){D(n,{where:t,connection:e.id})}}function M(e,t){A(e,p(t))}function N(e,t,n,r,i){E.refused+=1,M(e,{t:`err`,...t===void 0?{}:{n:t},...r===void 0?{}:{c:r},code:n,...i===void 0||i===``?{}:{msg:i}})}function P(e,t,n){e.closed||(j(e,`transport.send`,()=>e.transport.send(p({t:`bye`,code:t,why:n}))),j(e,`transport.close`,()=>e.transport.close(t,n)),Z(e))}function F(e){let t=S.get(e);return(t===void 0||t.closing)&&(t={name:e,subs:/* @__PURE__ */ new Set,bucket:new o(g.channelPublishesPerSecond,g.channelPublishesPerSecond,_()),adapterSub:void 0,presenceSub:void 0,closing:!1,frame:void 0},S.set(e,t)),t}async function I(e,t,n){if(e[t]===void 0){let r=n();e[t]=r,r.catch(()=>{e[t]=void 0})}await e[t]}function L(e,t){for(let n of e.subs)n.active&&(n.pumping?n.buffer.length>=1e4?H(n,`gap`):n.buffer.push(t):t.seq===n.lastSeq+1?R(n,t,!1):t.seq>n.lastSeq+1?(n.buffer.push(t),z(n,void 0)):t.at>n.lastAt&&H(n,`reset`))}function R(e,t,n){if(e.lastSeq=t.seq,e.lastAt=Math.max(e.lastAt,t.at),!e.echo&&t.publisher===e.conn.id)return;if(E.delivered+=1,n){A(e.conn,m(e.channel.name,t,!0));return}let r=e.channel.frame,i=r!==void 0&&r.seq===t.seq?r.text:m(e.channel.name,t,!1);(r===void 0||r.seq!==t.seq)&&(e.channel.frame={seq:t.seq,text:i}),A(e.conn,i)}async function z(e,t){if(!(e.pumping&&t===void 0)){e.pumping=!0;try{for(;e.active;){e.buffer=e.buffer.filter(t=>t.seq>e.lastSeq);let n=e.buffer[0];if(n!==void 0&&n.seq===e.lastSeq+1){R(e,n,!1),e.buffer.shift();continue}let r=n===void 0?t??0:n.seq-1;if(e.lastSeq>=r)break;let i=await y.history(e.channel.name,e.lastSeq,Math.min(500,r-e.lastSeq));if(!e.active)return;let a=i.messages[0];if(a===void 0||a.seq!==e.lastSeq+1){if(!e.gapOk){H(e,`gap`);return}e.lastSeq=(a?.seq??r+1)-1}else for(let t of i.messages){if(t.seq>r)break;R(e,t,!0)}}}catch(t){D(t,{where:`pump`,channel:e.channel.name,connection:e.conn.id}),e.active&&H(e,`unavailable`)}finally{e.pumping=!1}e.active&&e.buffer.length>0&&z(e,void 0)}}function B(e,t){for(let n of e.subs)n.joined&&t.op!==`leave`&&t.member.id===n.conn.id&&t.member.v>n.version&&(n.version=t.member.v,n.state=t.member.s),n.active&&(n.joined||n.watch)&&(n.presenceQueue===void 0?V(n,t):n.presenceQueue.push(t))}function V(e,t){M(e.conn,{t:`pres`,c:t.channel,op:t.op,m:t.member})}function H(e,t){e.active&&(e.active=!1,e.conn.subs.delete(e.channel.name),e.channel.subs.delete(e),t!==void 0&&!e.conn.closed&&M(e.conn,{t:`unsubbed`,c:e.channel.name,...t===``?{}:{why:t}}),W(e))}let U=/* @__PURE__ */ new Set;function W(e){let t=G(e);return U.add(t),t.finally(()=>U.delete(t)),t}async function G(e){let t=e.channel;try{await e.joining?.catch(()=>void 0),e.joined&&(e.joined=!1,await y.presenceRemove(t.name,e.conn.id))}catch(n){D(n,{where:`presenceRemove`,channel:t.name,connection:e.conn.id})}if(t.subs.size>0||t.closing)return;t.closing=!0,S.get(t.name)===t&&S.delete(t.name);let n=[t.adapterSub,t.presenceSub].filter(e=>e!==void 0);await Promise.all(n.map(async e=>{try{await(await e)()}catch(e){D(e,{where:`unsubscribe`,channel:t.name})}}))}function K(e){return e.identity.user===void 0?`unauthorized`:`forbidden`}async function q(n,r){let{c:i,n:a}=r;if(n.subs.has(i))return N(n,a,`conflict`,i,void 0);if(n.subs.size>=g.maxChannelsPerConnection)return N(n,a,`limit`,i,void 0);let o=O(i);if(o===void 0||!await k(o.rule,`subscribe`,i,o.params,n.identity))return N(n,a,K(n),i);let s=r.hasPresence||r.watch===!0;if(s&&!await k(o.rule,`presence`,i,o.params,n.identity))return N(n,a,K(n),i);if(r.hasPresence&&t(JSON.stringify(r.presence??null))>g.maxStateBytes)return N(n,a,`too_large`,i,void 0);if(n.closed)return;let c=F(i);if(c.subs.size>=g.maxSubscribersPerChannel)return N(n,a,`limit`,i,void 0);let l={conn:n,channel:c,rule:o.rule,params:o.params,echo:r.echo!==!1,watch:r.watch===!0,joined:!1,version:0,state:null,lastSeq:0,lastAt:_(),pumping:!0,gapOk:!1,buffer:[],presenceQueue:s?[]:void 0,joining:void 0,active:!0};n.subs.set(i,l),c.subs.add(l);try{await I(c,`adapterSub`,()=>y.subscribe(c.name,e=>L(c,e))),s&&(await I(c,`presenceSub`,()=>y.presenceSubscribe(c.name,e=>B(c,e))),Q());let e=await y.history(i,2**53-1,0);if(!l.active)return;let t=e.head,o=r.after,u=!1,d=!1;if(o===void 0?l.lastSeq=t:o>t?(d=!0,l.lastSeq=t):(l.lastSeq=o,u=e.oldest===0?t>o:e.oldest>o+1,l.gapOk=u),M(n,{t:`subbed`,n:a,c:i,head:t,...u?{gap:!0}:{},...d?{reset:!0}:{}}),r.hasPresence&&(l.joining=ee(l,r.presence??null),await l.joining),s&&await te(l),!l.active)return;await z(l,t),l.gapOk=!1}catch(t){l.active&&(H(l),D(t,{where:`subscribe`,channel:i,connection:n.id}),N(n,a,t instanceof e?t.code:`unavailable`,i))}}async function ee(t,n){if((await y.presenceList(t.channel.name)).length>=g.maxPresenceMembers)throw new e(`limit`,void 0);t.version=1,t.state=n,t.joined=!0,Q();let r={id:t.conn.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:n,v:1};await y.presenceSet(t.channel.name,r,g.presenceTtlMs)}async function te(e){let t=await y.presenceList(e.channel.name);if(!e.active)return;M(e.conn,{t:`pres`,c:e.channel.name,op:`sync`,m:t});let n=e.presenceQueue??[];e.presenceQueue=void 0;let r=new Map(t.map(e=>[e.id,e.v]));for(let t of n){let n=r.get(t.member.id);(t.op===`leave`?n===void 0:n!==void 0&&n>=t.member.v)||V(e,t)}}async function ne(e,n){let r=e.subs.get(n.c);if(r===void 0||!r.joined)return N(e,void 0,`not_subscribed`,n.c);if(t(JSON.stringify(n.s))>g.maxStateBytes)return N(e,void 0,`too_large`,n.c,void 0);if(!e.publishBucket.take(_()))return J(e,void 0,n.c);r.version+=1,r.state=n.s;let i={id:e.id,...e.identity.user===void 0?{}:{u:e.identity.user},s:n.s,v:r.version};try{await y.presenceSet(n.c,i,g.presenceTtlMs)}catch(t){D(t,{where:`presenceSet`,channel:n.c,connection:e.id}),N(e,void 0,`unavailable`,n.c)}}function J(e,t,n){N(e,t,`rate_limited`,n),e.limited.hit(_())&&P(e,1008,``)}async function re(e,t){let{c:n,n:r}=t,i=O(n);if(i===void 0||!await k(i.rule,`publish`,n,i.params,e.identity))return N(e,r,K(e),n);if(t.d.length>g.maxMessageBytes)return N(e,r,`too_large`,n,void 0);if(!e.publishBucket.take(_())||S.get(n)?.bucket.take(_())===!1)return J(e,r,n);try{let a=await y.publish(n,{event:t.e,data:t.d,publisher:e.id,...e.identity.user===void 0?{}:{user:e.identity.user}},{history:i.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...t.k===void 0?{}:{key:t.k}});E.published+=+!a.duplicate,r!==void 0&&M(e,{t:`ack`,n:r,s:a.seq,...a.duplicate?{dup:!0}:{}})}catch(t){D(t,{where:`publish`,channel:n,connection:e.id}),N(e,r,`unavailable`,n)}}async function ie(n,r){let{c:i,n:a}=r,o=n.subs.get(i);if(o===void 0&&n.stateless){let e=O(i);e!==void 0&&await k(e.rule,`subscribe`,i,e.params,n.identity)&&(o=e)}if(o===void 0)return N(n,a,K(n)===`unauthorized`&&n.stateless?`unauthorized`:`not_subscribed`,i);let s=o,c=s.rule.handlers?.[r.e];if(c===void 0)return N(n,a,`no_handler`,i);if(n.pending.size>=g.maxPendingRequests)return N(n,a,`limit`,i,void 0);if(!n.publishBucket.take(_()))return J(n,a,i);let l=new AbortController;n.pending.add(l);let u=Math.min(r.ms??g.requestTimeoutMs,g.requestTimeoutMs),d=setTimeout(()=>l.abort(new e(`timeout`)),u);(async()=>{try{let e=Promise.resolve(c({channel:i,params:s.params,identity:n.identity,event:r.e,data:r.d,signal:l.signal})),o=new Promise((e,t)=>{l.signal.addEventListener(`abort`,()=>t(l.signal.reason),{once:!0})}),u=await Promise.race([e,o])??null,d=JSON.stringify(u);if(d===void 0||t(d)>g.maxMessageBytes)return N(n,a,`too_large`,i,void 0);A(n,`{"t":"rep","n":${a},"d":${d}}`)}catch(t){t instanceof e?N(n,a,t.code,i,t.code===`timeout`||t.code===`closed`?void 0:t.message):(D(t,{where:`request`,channel:i,connection:n.id}),N(n,a,`handler_error`,i))}finally{clearTimeout(d),n.pending.delete(l),X(n)}})()}function Y(e,t,n,r){N(e,t,n,void 0,r),e.invalid.hit(_())&&P(e,1008,``)}async function ae(t,r){let i=n(r,g.maxMessageBytes);if(!i.ok){Y(t,i.n,i.code,i.why);return}let a=i.frame,o=`n`in a?a.n:void 0;if((a.t===`sub`||a.t===`unsub`)&&!t.controlBucket.take(_())&&a.t===`sub`)return J(t,o,a.c);try{switch(a.t){case`hello`:return;case`ping`:M(t,{t:`pong`,...a.n===void 0?{}:{n:a.n}});return;case`sub`:return await q(t,a);case`unsub`:{let e=t.subs.get(a.c);if(e===void 0)return N(t,a.n,`not_subscribed`,a.c);H(e);return}case`pub`:return await re(t,a);case`req`:return await ie(t,a);case`pres`:return await ne(t,a)}}catch(n){let r=`c`in a?a.c:void 0;n instanceof e?N(t,o,n.code,r):(D(n,{where:`frame`,connection:t.id,...r===void 0?{}:{channel:r}}),N(t,o,`unavailable`,r))}}async function oe(t){if(!t.running){t.running=!0;try{for(let n=t.queue.shift();n!==void 0&&!t.closed;n=t.queue.shift())try{await ae(t,n)}catch(n){n instanceof e?N(t,void 0,n.code):(D(n,{where:`frame`,connection:t.id}),N(t,void 0,`unavailable`))}}finally{t.running=!1,X(t)}}}function X(e){if(!(e.running||e.queue.length>0||e.pending.size>0))for(let t of e.waiters.splice(0))t()}async function Z(t){if(t.closed)return t.done;t.closed=!0,x.get(t.id)===t&&x.delete(t.id);for(let n of t.pending)n.abort(new e(`closed`));t.pending.clear(),t.queue.length=0;for(let e of t.waiters.splice(0))e();let n=[...t.subs.values()];for(let e of n)e.active=!1,e.channel.subs.delete(e);t.subs.clear(),await Promise.allSettled(n.map(e=>W(e))),t.release?.()}function Q(){if(C!==void 0||w)return;let e=setInterval(()=>void se(),g.presenceHeartbeatMs);e.unref?.(),C=e}async function se(){if(!T){T=!0;try{await ce()}finally{T=!1}}}async function ce(){let e=[],t=/* @__PURE__ */ new Set;for(let n of x.values())for(let r of n.subs.values())r.joined&&e.push({channel:r.channel.name,id:n.id}),(r.joined||r.watch)&&t.add(r.channel.name);if(e.length===0&&t.size===0)clearInterval(C),C=void 0;else try{let n=await y.presenceTouch(e,g.presenceTtlMs);for(let e of n){let t=x.get(e.id)?.subs.get(e.channel);t!==void 0&&t.joined&&await y.presenceSet(e.channel,{id:e.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:t.state,v:t.version},g.presenceTtlMs)}for(let e of t)await y.presenceReap(e)}catch(e){D(e,{where:`presenceHeartbeat`})}}for(let[e,t]of Object.entries(h.channels??{}))b.push(u(e,t));let $={limits:g,adapter:y,report:D,match:O,allowed:k,channel(e,t){return b.push(u(e,t)),$},connect(n,r={},i={}){if(w)throw new e(`closed`,void 0);let a=()=>void 0,c=new Promise(e=>{a=e}),l=i.actingFor===void 0?void 0:x.get(i.actingFor),u=l!==void 0&&r.user!==void 0&&l.identity.user===r.user,d={id:u?i.actingFor:f(),identity:r,transport:n,subs:/* @__PURE__ */ new Map,queue:[],publishBucket:i.publishBucket??new o(g.publishesPerSecond,g.publishBurst,_()),controlBucket:new o(g.publishesPerSecond,Math.max(g.publishBurst,g.maxChannelsPerConnection),_()),stateless:i.stateless===!0,waiters:[],invalid:new s(10,6e4),limited:new s(50,1e4),pending:/* @__PURE__ */ new Set,running:!1,closed:!1,release:a,done:c};return u||x.set(d.id,d),M(d,{t:`ready`,v:1,id:d.id,hb:g.heartbeatMs,max:g.maxMessageBytes}),{id:d.id,identity:r,done:c,receive(e){d.closed||(t(e)>g.maxMessageBytes?Y(d,void 0,`too_large`):d.queue.length>=g.maxQueuedFrames?P(d,1013,``):(d.queue.push(e),oe(d)))},closed(){Z(d)},settled(){return new Promise(e=>{d.waiters.push(e),X(d)})}}},async publish(n,o,s,c={}){if(!r(n))throw new e(`bad_channel`,void 0);if(!i(o)||c.key!==void 0&&!a(c.key))throw new e(`bad_frame`,void 0);let l;try{l=JSON.stringify(s===void 0?null:s)}catch(e){throw TypeError(`REALTIME_DATA`,{cause:e})}if(l===void 0)throw TypeError(`REALTIME_DATA`);if(t(l)>g.maxMessageBytes)throw new e(`too_large`,void 0);let u=O(n),d=await y.publish(n,{event:o,data:l},{history:u?.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...c.key===void 0?{}:{key:c.key}});return E.published+=+!d.duplicate,d},async setMemberState(n,r,i,a){if(n.user===void 0)throw new e(`unauthorized`,void 0);let o=O(r);if(o===void 0||!await k(o.rule,`presence`,r,o.params,n))throw new e(`forbidden`);if(t(JSON.stringify(a))>g.maxStateBytes)throw new e(`too_large`,void 0);let s=(await y.presenceList(r)).find(e=>e.id===i);if(s===void 0||s.u!==n.user)throw new e(`forbidden`,void 0);let c=x.get(i)?.subs.get(r),l=Math.max(s.v,c?.version??0)+1;c?.joined===!0&&(c.version=l,c.state=a),await y.presenceSet(r,{id:s.id,u:s.u,s:a,v:l},g.presenceTtlMs)},revoke(e){if(e.user===void 0&&e.connection===void 0&&e.channel===void 0)throw TypeError(`REALTIME_REVOKE`);let t=0;for(let n of[...x.values()])if((e.connection===void 0||n.id===e.connection)&&(e.user===void 0||n.identity.user===e.user))for(let r of[...n.subs.values()])(e.channel===void 0||r.channel.name===e.channel)&&(H(r,`revoked`),t+=1);return t},stats(){let e=0;for(let t of x.values())e+=t.subs.size;return{connections:x.size,channels:S.size,subscriptions:e,...E}},async close(){if(w)return;w=!0,C!==void 0&&clearInterval(C),C=void 0;let e=[...x.values()];for(let t of e)P(t,1001,``);await Promise.all(e.map(e=>e.done)),await Promise.allSettled([...U]),v&&await y.close()}};return $}export{h as createHubCore};
|
|
2
2
|
//# sourceMappingURL=hub.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub.js","names":["randomId","bytes","text","byte","encodeFrame","frame","messageFrame","channel","message","replayed","createHubCore","limits","resolveLimits","options","now","ownsAdapter","adapter","memoryAdapter","rules","conns","chans","heartbeat","closing","beating","counters","report","error","context","match","compiled","params","matchRule","allowed","permission","rule","op","RealtimeError","send","conn","drop","closeConnection","sendFrame","fail","n","reason","code","channelState","chan","name","TokenBucket","ensureAdapterSub","onMessage","ensurePresenceSub","onPresence","event","sub","removeSub","deliver","pump","cached","upTo","next","target","page","first","sendPresence","why","releaseSub","releasing","running","release","stops","item","stop","identityDenied","subscribe","found","wantsPresence","byteLength","startHeartbeat","probe","head","after","gap","reset","joinPresence","syncPresence","member","state","members","queued","known","version","setPresence","limit","publishFrame","result","request","handler","controller","timeout","timer","run","aborted","reject","reply","settle","handle","decoded","decodeClientFrame","wake","subs","beat","beatOnce","refs","watched","missing","ref","pattern","compileRule","core","done","resolve","owner","connectOptions","acting","identity","WindowCounter","isChannelName","isEventName","publishOptions","isIdempotencyKey","data","connectionId","count","subscriptions","all"],"sources":["../../../src/server/hub.ts"],"sourcesContent":["import type { Adapter, PublishResult, Unsubscribe } from \"../adapter\";\nimport { RealtimeError } from \"../errors\";\nimport { memoryAdapter } from \"../memory\";\nimport {\n byteLength,\n type ClientFrame,\n decodeClientFrame,\n isChannelName,\n isEventName,\n isIdempotencyKey,\n type Json,\n type Member,\n PROTOCOL_VERSION,\n type PresenceEvent,\n type StoredMessage,\n} from \"../protocol\";\nimport { type HubLimits, resolveLimits, TokenBucket, WindowCounter } from \"./limits\";\nimport {\n type ChannelRule,\n type CompiledRule,\n compileRule,\n type Identity,\n matchRule,\n type RuleContext,\n} from \"./rules\";\n\n/**\n * What a hub needs from a connection's transport. A WebSocket, an SSE stream and an in-memory test client all fit.\n * @since 1.0.0\n */\nexport interface Transport {\n /** Sends one frame (text). False when the transport is closed. */\n send(frame: string): boolean;\n /** Closes the transport with a WebSocket close code and a reason of at most 123 bytes. */\n close(code: number, reason: string): void;\n /** Bytes accepted by `send` that the peer has not yet taken. When present, a connection over the limit is closed. */\n buffered?(): number;\n}\n\n/** Where an error reported to `onError` came from. @since 1.0.0 */\nexport interface ErrorContext {\n readonly where: string;\n readonly channel?: string;\n readonly connection?: string;\n}\n\n/** Options of {@link createHub}. @since 1.0.0 */\nexport interface HubOptions {\n /** Storage and fan-out. Default: a fresh `memoryAdapter()`, closed with the hub. */\n readonly adapter?: Adapter;\n /** Channel rules by pattern, in order. The first matching pattern wins. */\n readonly channels?: Readonly<Record<string, ChannelRule>>;\n /** Limits (see {@link HubLimits}). */\n readonly limits?: Partial<HubLimits>;\n /** Receives failures the hub handled (an adapter outage, a rule predicate that threw). Default: `console.error`. */\n readonly onError?: (error: unknown, context: ErrorContext) => void;\n /** The clock, in epoch milliseconds. Default `Date.now`. */\n readonly now?: () => number;\n}\n\n/** Counters of a running hub. @since 1.0.0 */\nexport interface HubStats {\n readonly connections: number;\n readonly channels: number;\n readonly subscriptions: number;\n readonly published: number;\n readonly delivered: number;\n readonly refused: number;\n readonly slowConsumers: number;\n}\n\n/** One connection, as the transport glue drives it. @since 1.0.0 */\nexport interface HubConnection {\n readonly id: string;\n readonly identity: Identity;\n /** Hands the hub one text frame from the peer. Frames are handled in order. */\n receive(text: string): void;\n /** Tells the hub the transport closed. Safe to call twice. */\n closed(): void;\n /** Resolves once every frame received so far was handled and every request answered. */\n settled(): Promise<void>;\n /** Resolves when the hub finished releasing what the connection held. */\n readonly done: Promise<void>;\n}\n\n/** How a connection differs from a long-lived one. @since 1.0.0 */\nexport interface ConnectOptions {\n /**\n * A connection that exists for one HTTP request: a request is answered for any identity the channel's `subscribe` rule\n * accepts (there is no subscription to look up), and nothing is kept after it closes.\n */\n readonly stateless?: boolean;\n /** Replaces the connection's own publish bucket, so a caller can share one across requests. */\n readonly publishBucket?: TokenBucket;\n /**\n * The id of a long-lived connection of the same user this request acts for (SSE clients post under their stream's id, so\n * `echo:false` and `publisher` work). Honored only when that connection is open on this server and has the same user.\n */\n readonly actingFor?: string;\n}\n\n/** Who to remove subscriptions of. At least one field. @since 1.0.0 */\nexport interface RevokeTarget {\n readonly user?: string;\n readonly connection?: string;\n readonly channel?: string;\n}\n\n/** The server object. @since 1.0.0 */\nexport interface HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: ChannelRule): HubCore;\n /** Starts handling a connection. Sends `ready`. */\n connect(transport: Transport, identity?: Identity, options?: ConnectOptions): HubConnection;\n /** Publishes from the server (no rule applies). */\n publish(\n channel: string,\n event: string,\n data: unknown,\n options?: { readonly key?: string },\n ): Promise<PublishResult>;\n /** Removes matching subscriptions; each subscriber gets `unsubbed why:\"revoked\"`. Returns how many were removed. */\n revoke(target: RevokeTarget): number;\n /** Counters. */\n stats(): HubStats;\n /** Closes every connection with 1001, stops timers, removes presence, settles requests. */\n close(): Promise<void>;\n /** @internal Changes the state of a presence member from outside its connection (SSE clients). */\n setMemberState(\n identity: Identity,\n channel: string,\n connectionId: string,\n state: Json,\n ): Promise<void>;\n /** @internal The resolved limits. */\n readonly limits: HubLimits;\n /** @internal The adapter. */\n readonly adapter: Adapter;\n /** @internal Reports an error. */\n report(error: unknown, context: ErrorContext): void;\n /** @internal Matches a channel against the rules. */\n match(\n channel: string,\n ): { readonly rule: ChannelRule; readonly params: Record<string, string> } | undefined;\n /** @internal Evaluates a rule permission for an identity. */\n allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean>;\n}\n\ninterface SubState {\n readonly conn: ConnState;\n readonly channel: ChanState;\n readonly rule: ChannelRule;\n readonly params: Record<string, string>;\n readonly echo: boolean;\n readonly watch: boolean;\n joined: boolean;\n version: number;\n lastSeq: number;\n /** The `at` of the last message delivered: a message with an old seq but a newer `at` means the numbering restarted. */\n lastAt: number;\n /** While true, live messages wait in `buffer` and `pump` delivers in order. */\n pumping: boolean;\n gapOk: boolean;\n buffer: StoredMessage[];\n presenceQueue: PresenceEvent[] | undefined;\n /** The join in progress, so that releasing the subscription waits for it and never leaves a member behind. */\n joining: Promise<void> | undefined;\n active: boolean;\n}\n\ninterface ChanState {\n readonly name: string;\n readonly subs: Set<SubState>;\n readonly bucket: TokenBucket;\n adapterSub: Promise<Unsubscribe> | undefined;\n presenceSub: Promise<Unsubscribe> | undefined;\n closing: boolean;\n /** Cache of the encoded frame of the message being fanned out. */\n frame: { readonly seq: number; readonly text: string } | undefined;\n}\n\ninterface ConnState {\n readonly id: string;\n readonly identity: Identity;\n readonly transport: Transport;\n readonly subs: Map<string, SubState>;\n readonly queue: string[];\n readonly publishBucket: TokenBucket;\n readonly invalid: WindowCounter;\n readonly limited: WindowCounter;\n readonly pending: Set<AbortController>;\n readonly stateless: boolean;\n readonly waiters: (() => void)[];\n running: boolean;\n closed: boolean;\n release: (() => void) | undefined;\n readonly done: Promise<void>;\n}\n\nconst REPLAY_PAGE = 500;\nconst MAX_PUMP_BUFFER = 10_000;\nconst ID_BYTES = 16;\n\n/** A random id: 128 bits, base64url. */\nfunction randomId(): string {\n const bytes = new Uint8Array(ID_BYTES);\n crypto.getRandomValues(bytes);\n let text = \"\";\n for (const byte of bytes) text += String.fromCharCode(byte);\n return btoa(text).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction encodeFrame(frame: Record<string, unknown>): string {\n return JSON.stringify(frame);\n}\n\nfunction messageFrame(channel: string, message: StoredMessage, replayed: boolean): string {\n return (\n `{\"t\":\"msg\",\"c\":${JSON.stringify(channel)},\"s\":${message.seq},\"e\":${JSON.stringify(message.event)},\"d\":${message.data}` +\n (message.publisher === undefined ? \"\" : `,\"p\":${JSON.stringify(message.publisher)}`) +\n (message.user === undefined ? \"\" : `,\"u\":${JSON.stringify(message.user)}`) +\n (replayed ? ',\"r\":1}' : \"}\")\n );\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules, enforces limits and talks to\n * one adapter. Wire it to a transport with `hub.websocket()`, `hub.sse()` and `hub.post()` (Dusk routes) or with `hub.connect`.\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: { \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined } },\n * });\n * const sent: string[] = [];\n * const connection = hub.connect({ send: (frame) => (sent.push(frame), true), close: () => {} });\n * connection.receive('{\"t\":\"sub\",\"n\":1,\"c\":\"chat:lobby\"}');\n * @since 1.0.0\n */\nexport function createHubCore(options: HubOptions = {}): HubCore {\n const limits = resolveLimits(options.limits);\n const now = options.now ?? Date.now;\n const ownsAdapter = options.adapter === undefined;\n const adapter = options.adapter ?? memoryAdapter({ now });\n const rules: CompiledRule[] = [];\n const conns = new Map<string, ConnState>();\n const chans = new Map<string, ChanState>();\n let heartbeat: ReturnType<typeof setInterval> | undefined;\n let closing = false;\n let beating = false;\n const counters = { published: 0, delivered: 0, refused: 0, slowConsumers: 0 };\n\n const report = (error: unknown, context: ErrorContext): void => {\n if (options.onError !== undefined) {\n try {\n options.onError(error, context);\n } catch {\n // A failing reporter must not break the hub.\n }\n } else {\n console.error(`[realtime] ${context.where}`, error);\n }\n };\n\n function match(\n channel: string,\n ): { rule: ChannelRule; params: Record<string, string> } | undefined {\n for (const compiled of rules) {\n const params = matchRule(compiled, channel);\n if (params !== undefined) return { rule: compiled.rule, params };\n }\n return undefined;\n }\n\n async function allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean> {\n const permission = rule[op];\n if (permission === undefined || permission === false) return false;\n if (permission === true) return true;\n try {\n return (await permission({ channel, params, identity, op })) === true;\n } catch (error) {\n report(error, { where: `rule.${op}`, channel });\n throw new RealtimeError(\"unavailable\", \"the channel rule could not be evaluated\");\n }\n }\n\n function send(conn: ConnState, text: string): void {\n if (conn.closed) return;\n try {\n const buffered = conn.transport.buffered?.() ?? 0;\n if (buffered > limits.maxBufferedBytes) {\n counters.slowConsumers += 1;\n drop(conn, 1013, \"slow consumer\");\n return;\n }\n if (conn.transport.send(text)) return;\n } catch (error) {\n // A transport that throws is a broken socket: this connection goes, the others are not affected (guarantee G13).\n report(error, { where: \"transport.send\", connection: conn.id });\n }\n void closeConnection(conn);\n }\n\n function sendFrame(conn: ConnState, frame: Record<string, unknown>): void {\n send(conn, encodeFrame(frame));\n }\n\n function fail(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n channel?: string,\n message?: string,\n ): void {\n counters.refused += 1;\n sendFrame(conn, {\n t: \"err\",\n ...(n === undefined ? {} : { n }),\n ...(channel === undefined ? {} : { c: channel }),\n code,\n ...(message === undefined ? {} : { msg: message }),\n });\n }\n\n /** Sends a final frame, closes the transport and releases the connection. */\n function drop(conn: ConnState, code: number, reason: string): void {\n if (conn.closed) return;\n try {\n conn.transport.send(encodeFrame({ t: \"bye\", code, why: reason }));\n } catch {\n // The transport may already be gone.\n }\n try {\n conn.transport.close(code, reason);\n } catch {\n // See above.\n }\n void closeConnection(conn);\n }\n\n // --- channels ------------------------------------------------------------------------------------------------------\n\n function channelState(name: string): ChanState {\n let chan = chans.get(name);\n if (chan === undefined || chan.closing) {\n chan = {\n name,\n subs: new Set(),\n bucket: new TokenBucket(\n limits.channelPublishesPerSecond,\n limits.channelPublishesPerSecond,\n now(),\n ),\n adapterSub: undefined,\n presenceSub: undefined,\n closing: false,\n frame: undefined,\n };\n chans.set(name, chan);\n }\n return chan;\n }\n\n async function ensureAdapterSub(chan: ChanState): Promise<void> {\n if (chan.adapterSub === undefined) {\n chan.adapterSub = adapter.subscribe(chan.name, (message) => onMessage(chan, message));\n chan.adapterSub.catch(() => {\n chan.adapterSub = undefined;\n });\n }\n await chan.adapterSub;\n }\n\n async function ensurePresenceSub(chan: ChanState): Promise<void> {\n if (chan.presenceSub === undefined) {\n chan.presenceSub = adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event));\n chan.presenceSub.catch(() => {\n chan.presenceSub = undefined;\n });\n }\n await chan.presenceSub;\n }\n\n function onMessage(chan: ChanState, message: StoredMessage): void {\n for (const sub of chan.subs) {\n if (!sub.active) continue;\n if (sub.pumping) {\n // A consumer so far behind that it cannot catch up is resubscribed from its last seq instead of holding memory.\n if (sub.buffer.length >= MAX_PUMP_BUFFER) removeSub(sub, \"gap\");\n else sub.buffer.push(message);\n continue;\n }\n if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);\n else if (message.seq > sub.lastSeq + 1) {\n sub.buffer.push(message);\n void pump(sub, undefined);\n } else if (message.at > sub.lastAt) removeSub(sub, \"reset\");\n // Any other seq at or below the cursor was already delivered: dropped (guarantee G2).\n }\n }\n\n function deliver(sub: SubState, message: StoredMessage, replayed: boolean): void {\n sub.lastSeq = message.seq;\n sub.lastAt = Math.max(sub.lastAt, message.at);\n if (!sub.echo && message.publisher === sub.conn.id) return;\n counters.delivered += 1;\n if (replayed) {\n send(sub.conn, messageFrame(sub.channel.name, message, true));\n return;\n }\n const cached = sub.channel.frame;\n const text =\n cached !== undefined && cached.seq === message.seq\n ? cached.text\n : messageFrame(sub.channel.name, message, false);\n if (cached === undefined || cached.seq !== message.seq)\n sub.channel.frame = { seq: message.seq, text };\n send(sub.conn, text);\n }\n\n /**\n * Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the\n * missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one\n * is removed with `unsubbed why:\"gap\"`.\n */\n async function pump(sub: SubState, upTo: number | undefined): Promise<void> {\n if (sub.pumping && upTo === undefined) return;\n sub.pumping = true;\n try {\n while (sub.active) {\n sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);\n const next = sub.buffer[0];\n if (next !== undefined && next.seq === sub.lastSeq + 1) {\n deliver(sub, next, false);\n sub.buffer.shift();\n continue;\n }\n const target = next === undefined ? (upTo ?? 0) : next.seq - 1;\n if (sub.lastSeq >= target) break;\n const page = await adapter.history(\n sub.channel.name,\n sub.lastSeq,\n Math.min(REPLAY_PAGE, target - sub.lastSeq),\n );\n if (!sub.active) return;\n const first = page.messages[0];\n if (first === undefined || first.seq !== sub.lastSeq + 1) {\n if (!sub.gapOk) {\n removeSub(sub, \"gap\");\n return;\n }\n // The gap was reported at subscribe time: continue from what is retained.\n sub.lastSeq = (first?.seq ?? target + 1) - 1;\n continue;\n }\n for (const message of page.messages) {\n if (message.seq > target) break;\n deliver(sub, message, true);\n }\n }\n } catch (error) {\n report(error, { where: \"pump\", channel: sub.channel.name, connection: sub.conn.id });\n if (sub.active) removeSub(sub, \"unavailable\");\n } finally {\n sub.pumping = false;\n }\n // A message that arrived while the last history page was awaited is in the buffer; one more pass delivers it.\n if (sub.active && sub.buffer.length > 0) void pump(sub, undefined);\n }\n\n function onPresence(chan: ChanState, event: PresenceEvent): void {\n for (const sub of chan.subs) {\n if (!sub.active || (!sub.joined && !sub.watch)) continue;\n if (sub.presenceQueue !== undefined) sub.presenceQueue.push(event);\n else sendPresence(sub, event);\n }\n }\n\n function sendPresence(sub: SubState, event: PresenceEvent): void {\n sendFrame(sub.conn, { t: \"pres\", c: event.channel, op: event.op, m: event.member });\n }\n\n /** Removes a subscription from its connection and channel; tells the peer when `why` is given. */\n function removeSub(sub: SubState, why?: string): void {\n if (!sub.active) return;\n sub.active = false;\n sub.conn.subs.delete(sub.channel.name);\n sub.channel.subs.delete(sub);\n if (why !== undefined && !sub.conn.closed)\n sendFrame(sub.conn, { t: \"unsubbed\", c: sub.channel.name, ...(why === \"\" ? {} : { why }) });\n void releaseSub(sub);\n }\n\n /** The releases still running, so that `close` finishes them before it closes the adapter. */\n const releasing = new Set<Promise<void>>();\n\n function releaseSub(sub: SubState): Promise<void> {\n const running = release(sub);\n releasing.add(running);\n void running.finally(() => releasing.delete(running));\n return running;\n }\n\n async function release(sub: SubState): Promise<void> {\n const chan = sub.channel;\n try {\n // A join that is still being written must finish first, or its member would outlive the connection.\n await sub.joining?.catch(() => undefined);\n if (sub.joined) {\n sub.joined = false;\n await adapter.presenceRemove(chan.name, sub.conn.id);\n }\n } catch (error) {\n report(error, { where: \"presenceRemove\", channel: chan.name, connection: sub.conn.id });\n }\n if (chan.subs.size > 0 || chan.closing) return;\n chan.closing = true;\n if (chans.get(chan.name) === chan) chans.delete(chan.name);\n const stops = [chan.adapterSub, chan.presenceSub].filter(\n (item): item is Promise<Unsubscribe> => item !== undefined,\n );\n await Promise.all(\n stops.map(async (stop) => {\n try {\n await (await stop)();\n } catch (error) {\n report(error, { where: \"unsubscribe\", channel: chan.name });\n }\n }),\n );\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n function identityDenied(conn: ConnState): RealtimeError[\"code\"] {\n return conn.identity.user === undefined ? \"unauthorized\" : \"forbidden\";\n }\n\n async function subscribe(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"sub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n if (conn.subs.has(name)) return fail(conn, n, \"conflict\", name, \"already subscribed\");\n if (conn.subs.size >= limits.maxChannelsPerConnection)\n return fail(conn, n, \"limit\", name, \"too many channels on this connection\");\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n const wantsPresence = frame.hasPresence || frame.watch === true;\n if (\n wantsPresence &&\n !(await allowed(found.rule, \"presence\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (\n frame.hasPresence &&\n byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes\n )\n return fail(conn, n, \"too_large\", name, \"the presence state is too large\");\n if (conn.closed) return;\n const chan = channelState(name);\n if (chan.subs.size >= limits.maxSubscribersPerChannel)\n return fail(conn, n, \"limit\", name, \"the channel is full\");\n const sub: SubState = {\n conn,\n channel: chan,\n rule: found.rule,\n params: found.params,\n echo: frame.echo !== false,\n watch: frame.watch === true,\n joined: false,\n version: 0,\n lastSeq: 0,\n lastAt: now(),\n pumping: true,\n gapOk: false,\n buffer: [],\n presenceQueue: wantsPresence ? [] : undefined,\n joining: undefined,\n active: true,\n };\n conn.subs.set(name, sub);\n chan.subs.add(sub);\n try {\n await ensureAdapterSub(chan);\n if (wantsPresence) {\n await ensurePresenceSub(chan);\n startHeartbeat();\n }\n const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);\n if (!sub.active) return;\n const head = probe.head;\n const after = frame.after;\n let gap = false;\n let reset = false;\n if (after === undefined) {\n sub.lastSeq = head;\n } else if (after > head) {\n reset = true;\n sub.lastSeq = head;\n } else {\n sub.lastSeq = after;\n gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;\n sub.gapOk = gap;\n }\n sendFrame(conn, {\n t: \"subbed\",\n n,\n c: name,\n head,\n ...(gap ? { gap: true } : {}),\n ...(reset ? { reset: true } : {}),\n });\n if (frame.hasPresence) {\n sub.joining = joinPresence(sub, frame.presence ?? null);\n await sub.joining;\n }\n if (wantsPresence) await syncPresence(sub);\n if (!sub.active) return;\n await pump(sub, head);\n sub.gapOk = false;\n } catch (error) {\n if (sub.active) {\n removeSub(sub);\n report(error, { where: \"subscribe\", channel: name, connection: conn.id });\n fail(conn, n, error instanceof RealtimeError ? error.code : \"unavailable\", name);\n }\n }\n }\n\n async function joinPresence(sub: SubState, state: Json): Promise<void> {\n const existing = await adapter.presenceList(sub.channel.name);\n if (existing.length >= limits.maxPresenceMembers)\n throw new RealtimeError(\"limit\", \"the presence set is full\");\n sub.version = 1;\n sub.joined = true;\n startHeartbeat();\n const member: Member = {\n id: sub.conn.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: state,\n v: 1,\n };\n await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);\n }\n\n async function syncPresence(sub: SubState): Promise<void> {\n const members = await adapter.presenceList(sub.channel.name);\n if (!sub.active) return;\n sendFrame(sub.conn, { t: \"pres\", c: sub.channel.name, op: \"sync\", m: members });\n const queued = sub.presenceQueue ?? [];\n sub.presenceQueue = undefined;\n const known = new Map(members.map((member) => [member.id, member.v]));\n for (const event of queued) {\n // What the snapshot already shows is not announced again.\n const version = known.get(event.member.id);\n if (\n event.op === \"leave\"\n ? version === undefined\n : version !== undefined && version >= event.member.v\n )\n continue;\n sendPresence(sub, event);\n }\n }\n\n async function setPresence(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pres\" }>,\n ): Promise<void> {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined || !sub.joined) return fail(conn, undefined, \"not_subscribed\", frame.c);\n if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes)\n return fail(conn, undefined, \"too_large\", frame.c, \"the presence state is too large\");\n if (!conn.publishBucket.take(now())) return limit(conn, undefined, frame.c);\n sub.version += 1;\n const member: Member = {\n id: conn.id,\n ...(conn.identity.user === undefined ? {} : { u: conn.identity.user }),\n s: frame.s,\n v: sub.version,\n };\n try {\n await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);\n } catch (error) {\n report(error, { where: \"presenceSet\", channel: frame.c, connection: conn.id });\n fail(conn, undefined, \"unavailable\", frame.c);\n }\n }\n\n function limit(conn: ConnState, n: number | undefined, channel: string): void {\n fail(conn, n, \"rate_limited\", channel);\n if (conn.limited.hit(now())) drop(conn, 1008, \"rate limit exceeded\");\n }\n\n async function publishFrame(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"publish\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (frame.d.length > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, \"the message is too large\");\n // A channel with no subscriber on this server has no bucket: only the connection's own rate applies to it.\n if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false)\n return limit(conn, n, name);\n try {\n const result = await adapter.publish(\n name,\n {\n event: frame.e,\n data: frame.d,\n publisher: conn.id,\n ...(conn.identity.user === undefined ? {} : { user: conn.identity.user }),\n },\n {\n history: found.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(frame.k === undefined ? {} : { key: frame.k }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n if (n !== undefined)\n sendFrame(conn, { t: \"ack\", n, s: result.seq, ...(result.duplicate ? { dup: true } : {}) });\n } catch (error) {\n report(error, { where: \"publish\", channel: name, connection: conn.id });\n fail(conn, n, \"unavailable\", name);\n }\n }\n\n async function request(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"req\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n let target: { rule: ChannelRule; params: Record<string, string> } | undefined =\n conn.subs.get(name);\n if (target === undefined && conn.stateless) {\n const found = match(name);\n if (\n found !== undefined &&\n (await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n target = found;\n }\n if (target === undefined)\n return fail(\n conn,\n n,\n identityDenied(conn) === \"unauthorized\" && conn.stateless\n ? \"unauthorized\"\n : \"not_subscribed\",\n name,\n );\n const sub = target;\n const handler = sub.rule.handlers?.[frame.e];\n if (handler === undefined) return fail(conn, n, \"no_handler\", name);\n if (conn.pending.size >= limits.maxPendingRequests)\n return fail(conn, n, \"limit\", name, \"too many requests in flight\");\n if (!conn.publishBucket.take(now())) return limit(conn, n, name);\n const controller = new AbortController();\n conn.pending.add(controller);\n const timeout = Math.min(frame.ms ?? limits.requestTimeoutMs, limits.requestTimeoutMs);\n const timer = setTimeout(() => controller.abort(new RealtimeError(\"timeout\")), timeout);\n void (async () => {\n try {\n const run = Promise.resolve(\n handler({\n channel: name,\n params: sub.params,\n identity: conn.identity,\n event: frame.e,\n data: frame.d,\n signal: controller.signal,\n }),\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n controller.signal.addEventListener(\"abort\", () => reject(controller.signal.reason), {\n once: true,\n });\n });\n const reply = (await Promise.race([run, aborted])) ?? null;\n const text = JSON.stringify(reply);\n if (text === undefined || byteLength(text) > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, \"the reply is too large\");\n send(conn, `{\"t\":\"rep\",\"n\":${n},\"d\":${text}}`);\n } catch (error) {\n if (error instanceof RealtimeError)\n fail(\n conn,\n n,\n error.code,\n name,\n error.code === \"timeout\" || error.code === \"closed\" ? undefined : error.message,\n );\n else {\n report(error, { where: \"request\", channel: name, connection: conn.id });\n fail(conn, n, \"handler_error\", name);\n }\n } finally {\n clearTimeout(timer);\n conn.pending.delete(controller);\n settle(conn);\n }\n })();\n }\n\n async function handle(conn: ConnState, text: string): Promise<void> {\n const decoded = decodeClientFrame(text, limits.maxMessageBytes);\n if (!decoded.ok) {\n fail(conn, decoded.n, decoded.code, undefined, decoded.why);\n if (conn.invalid.hit(now())) drop(conn, 1008, \"too many invalid frames\");\n return;\n }\n const frame = decoded.frame;\n try {\n switch (frame.t) {\n case \"hello\":\n return;\n case \"ping\":\n sendFrame(conn, { t: \"pong\", ...(frame.n === undefined ? {} : { n: frame.n }) });\n return;\n case \"sub\":\n return await subscribe(conn, frame);\n case \"unsub\": {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined) return fail(conn, frame.n, \"not_subscribed\", frame.c);\n removeSub(sub);\n return;\n }\n case \"pub\":\n return await publishFrame(conn, frame);\n case \"req\":\n return await request(conn, frame);\n case \"pres\":\n return await setPresence(conn, frame);\n }\n } catch (error) {\n const n = \"n\" in frame ? frame.n : undefined;\n const channel = \"c\" in frame ? frame.c : undefined;\n if (error instanceof RealtimeError) fail(conn, n, error.code, channel);\n else {\n report(error, {\n where: \"frame\",\n connection: conn.id,\n ...(channel === undefined ? {} : { channel }),\n });\n fail(conn, n, \"unavailable\", channel);\n }\n }\n }\n\n async function run(conn: ConnState): Promise<void> {\n if (conn.running) return;\n conn.running = true;\n try {\n for (let text = conn.queue.shift(); text !== undefined; text = conn.queue.shift()) {\n if (conn.closed) break;\n try {\n await handle(conn, text);\n } catch (error) {\n if (error instanceof RealtimeError) fail(conn, undefined, error.code);\n else {\n report(error, { where: \"frame\", connection: conn.id });\n fail(conn, undefined, \"unavailable\");\n }\n }\n }\n } finally {\n conn.running = false;\n settle(conn);\n }\n }\n\n /** Wakes the callers of `settled` when the connection has nothing left to do. */\n function settle(conn: ConnState): void {\n if (conn.running || conn.queue.length > 0 || conn.pending.size > 0) return;\n for (const wake of conn.waiters.splice(0)) wake();\n }\n\n async function closeConnection(conn: ConnState): Promise<void> {\n if (conn.closed) return conn.done;\n conn.closed = true;\n if (conns.get(conn.id) === conn) conns.delete(conn.id);\n for (const controller of conn.pending) controller.abort(new RealtimeError(\"closed\"));\n conn.pending.clear();\n conn.queue.length = 0;\n for (const wake of conn.waiters.splice(0)) wake();\n const subs = [...conn.subs.values()];\n for (const sub of subs) {\n sub.active = false;\n sub.channel.subs.delete(sub);\n }\n conn.subs.clear();\n await Promise.allSettled(subs.map((sub) => releaseSub(sub)));\n conn.release?.();\n }\n\n // --- presence heartbeat --------------------------------------------------------------------------------------------\n\n function startHeartbeat(): void {\n if (heartbeat !== undefined || closing) return;\n const timer = setInterval(() => void beat(), limits.presenceHeartbeatMs);\n // Node's timer can be unref'd so an idle hub does not keep the process alive; other runtimes return a number.\n (timer as unknown as { unref?: () => void }).unref?.();\n heartbeat = timer;\n }\n\n async function beat(): Promise<void> {\n if (beating) return;\n beating = true;\n try {\n await beatOnce();\n } finally {\n beating = false;\n }\n }\n\n async function beatOnce(): Promise<void> {\n const refs: { channel: string; id: string }[] = [];\n const watched = new Set<string>();\n for (const conn of conns.values())\n for (const sub of conn.subs.values()) {\n if (sub.joined) refs.push({ channel: sub.channel.name, id: conn.id });\n if (sub.joined || sub.watch) watched.add(sub.channel.name);\n }\n if (refs.length === 0 && watched.size === 0) {\n clearInterval(heartbeat);\n heartbeat = undefined;\n return;\n }\n try {\n const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);\n for (const ref of missing) {\n const sub = conns.get(ref.id)?.subs.get(ref.channel);\n if (sub === undefined || !sub.joined) continue;\n await adapter.presenceSet(\n ref.channel,\n {\n id: ref.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: null,\n v: sub.version,\n },\n limits.presenceTtlMs,\n );\n }\n for (const channel of watched) await adapter.presenceReap(channel);\n } catch (error) {\n report(error, { where: \"presenceHeartbeat\" });\n }\n }\n\n // --- the hub -------------------------------------------------------------------------------------------------------\n\n for (const [pattern, rule] of Object.entries(options.channels ?? {}))\n rules.push(compileRule(pattern, rule));\n\n const core: HubCore = {\n limits,\n adapter,\n report,\n match,\n allowed,\n\n channel(pattern, rule) {\n rules.push(compileRule(pattern, rule));\n return core;\n },\n\n connect(transport, identity = {}, connectOptions = {}) {\n if (closing) throw new RealtimeError(\"closed\", \"the hub is closed\");\n let release: () => void = () => undefined;\n const done = new Promise<void>((resolve) => {\n release = resolve;\n });\n const owner =\n connectOptions.actingFor === undefined ? undefined : conns.get(connectOptions.actingFor);\n const acting =\n owner !== undefined && identity.user !== undefined && owner.identity.user === identity.user;\n const conn: ConnState = {\n id: acting ? (connectOptions.actingFor as string) : randomId(),\n identity,\n transport,\n subs: new Map(),\n queue: [],\n publishBucket:\n connectOptions.publishBucket ??\n new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),\n stateless: connectOptions.stateless === true,\n waiters: [],\n invalid: new WindowCounter(10, 60_000),\n limited: new WindowCounter(50, 10_000),\n pending: new Set(),\n running: false,\n closed: false,\n release,\n done,\n };\n // An acting connection shares its owner's id: it must not replace the owner in the table.\n if (!acting) conns.set(conn.id, conn);\n sendFrame(conn, {\n t: \"ready\",\n v: PROTOCOL_VERSION,\n id: conn.id,\n hb: limits.heartbeatMs,\n max: limits.maxMessageBytes,\n });\n return {\n id: conn.id,\n identity,\n done,\n receive(text) {\n if (conn.closed) return;\n if (conn.queue.length >= limits.maxQueuedFrames) {\n drop(conn, 1013, \"too many frames waiting\");\n return;\n }\n conn.queue.push(text);\n void run(conn);\n },\n closed() {\n void closeConnection(conn);\n },\n settled() {\n return new Promise<void>((resolve) => {\n conn.waiters.push(resolve);\n settle(conn);\n });\n },\n };\n },\n\n async publish(channel, event, data, publishOptions = {}) {\n if (!isChannelName(channel))\n throw new RealtimeError(\"bad_channel\", `\"${channel}\" is not a valid channel name`);\n if (!isEventName(event))\n throw new RealtimeError(\"bad_frame\", `\"${event}\" is not a valid event name`);\n if (publishOptions.key !== undefined && !isIdempotencyKey(publishOptions.key))\n throw new RealtimeError(\"bad_frame\", \"the idempotency key is not valid\");\n let text: string | undefined;\n try {\n text = JSON.stringify(data === undefined ? null : data);\n } catch (error) {\n throw new TypeError(\"the data is not JSON\", { cause: error });\n }\n if (text === undefined) throw new TypeError(\"the data is not JSON\");\n if (byteLength(text) > limits.maxMessageBytes)\n throw new RealtimeError(\"too_large\", \"the message is too large\");\n const found = match(channel);\n const result = await adapter.publish(\n channel,\n { event, data: text },\n {\n history: found?.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(publishOptions.key === undefined ? {} : { key: publishOptions.key }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n return result;\n },\n\n async setMemberState(identity, channel, connectionId, state) {\n if (identity.user === undefined)\n throw new RealtimeError(\"unauthorized\", \"presence changes over HTTP need a signed-in user\");\n const found = match(channel);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"presence\", channel, found.params, identity))\n )\n throw new RealtimeError(\"forbidden\");\n if (byteLength(JSON.stringify(state)) > limits.maxStateBytes)\n throw new RealtimeError(\"too_large\", \"the presence state is too large\");\n const members = await adapter.presenceList(channel);\n const member = members.find((item) => item.id === connectionId);\n if (member === undefined || member.u !== identity.user)\n throw new RealtimeError(\"forbidden\", \"that connection is not yours\");\n await adapter.presenceSet(\n channel,\n { id: member.id, u: member.u, s: state, v: member.v + 1 },\n limits.presenceTtlMs,\n );\n },\n\n revoke(target) {\n if (\n target.user === undefined &&\n target.connection === undefined &&\n target.channel === undefined\n )\n throw new TypeError(\"revoke needs a user, a connection or a channel.\");\n let count = 0;\n for (const conn of [...conns.values()]) {\n if (target.connection !== undefined && conn.id !== target.connection) continue;\n if (target.user !== undefined && conn.identity.user !== target.user) continue;\n for (const sub of [...conn.subs.values()]) {\n if (target.channel !== undefined && sub.channel.name !== target.channel) continue;\n removeSub(sub, \"revoked\");\n count += 1;\n }\n }\n return count;\n },\n\n stats() {\n let subscriptions = 0;\n for (const conn of conns.values()) subscriptions += conn.subs.size;\n return {\n connections: conns.size,\n channels: chans.size,\n subscriptions,\n ...counters,\n };\n },\n\n async close() {\n if (closing) return;\n closing = true;\n if (heartbeat !== undefined) clearInterval(heartbeat);\n heartbeat = undefined;\n const all = [...conns.values()];\n for (const conn of all) drop(conn, 1001, \"going away\");\n await Promise.all(all.map((conn) => conn.done));\n await Promise.allSettled([...releasing]);\n if (ownsAdapter) await adapter.close();\n },\n };\n return core;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hub.js","names":["randomId","bytes","encodeFrame","frame","messageFrame","channel","message","replayed","createHubCore","limits","resolveLimits","options","now","ownsAdapter","adapter","memoryAdapter","report","error","context","rules","conns","chans","heartbeat","closing","beating","counters","match","compiled","params","matchRule","allowed","permission","rule","op","RealtimeError","send","conn","drop","text","onTransport","closeConnection","call","sendFrame","fail","n","reason","code","channelState","chan","name","TokenBucket","ensureFeed","key","opened","open","onMessage","sub","removeSub","deliver","pump","cached","upTo","next","target","page","first","onPresence","event","sendPresence","why","releaseSub","releasing","running","release","stops","item","stop","identityDenied","subscribe","found","wantsPresence","byteLength","startHeartbeat","probe","head","after","gap","reset","joinPresence","syncPresence","state","member","members","queued","known","version","setPresence","limit","publishFrame","result","request","handler","controller","timeout","timer","run","aborted","reject","reply","settle","refuseFrame","handle","decoded","decodeClientFrame","wake","subs","beat","beatOnce","refs","watched","missing","ref","pattern","compileRule","core","done","resolve","owner","connectOptions","acting","identity","WindowCounter","isChannelName","isEventName","publishOptions","isIdempotencyKey","data","connectionId","local","count","subscriptions","all"],"sources":["../../../src/server/hub.ts"],"sourcesContent":["import type { Adapter, PublishResult, Unsubscribe } from \"../adapter\";\nimport { RealtimeError } from \"../errors\";\nimport { memoryAdapter } from \"../memory\";\nimport {\n byteLength,\n type ClientFrame,\n decodeClientFrame,\n isChannelName,\n isEventName,\n isIdempotencyKey,\n type Json,\n type Member,\n PROTOCOL_VERSION,\n type PresenceEvent,\n type StoredMessage,\n} from \"../protocol\";\nimport { type HubLimits, resolveLimits, TokenBucket, WindowCounter } from \"./limits\";\nimport {\n type ChannelRule,\n type CompiledRule,\n compileRule,\n type Identity,\n matchRule,\n type RuleContext,\n} from \"./rules\";\n\n/**\n * What a hub needs from a connection's transport. A WebSocket, an SSE stream and an in-memory test client all fit.\n * @since 1.0.0\n */\nexport interface Transport {\n /** Sends one frame (text). False when the transport is closed. */\n send(frame: string): boolean;\n /** Closes the transport with a WebSocket close code and a reason of at most 123 bytes. */\n close(code: number, reason: string): void;\n /** Bytes accepted by `send` that the peer has not yet taken. When present, a connection over the limit is closed. */\n buffered?(): number;\n}\n\n/** Where an error reported to `onError` came from. @since 1.0.0 */\nexport interface ErrorContext {\n readonly where: string;\n readonly channel?: string;\n readonly connection?: string;\n}\n\n/** Options of {@link createHub}. @since 1.0.0 */\nexport interface HubOptions {\n /** Storage and fan-out. Default: a fresh `memoryAdapter()`, closed with the hub. */\n readonly adapter?: Adapter;\n /** Channel rules by pattern, in order. The first matching pattern wins. */\n readonly channels?: Readonly<Record<string, ChannelRule>>;\n /** Limits (see {@link HubLimits}). */\n readonly limits?: Partial<HubLimits>;\n /** Receives failures the hub handled (an adapter outage, a rule predicate that threw). Default: `console.error`. */\n readonly onError?: (error: unknown, context: ErrorContext) => void;\n /** The clock, in epoch milliseconds. Default `Date.now`. */\n readonly now?: () => number;\n}\n\n/** Counters of a running hub. @since 1.0.0 */\nexport interface HubStats {\n readonly connections: number;\n readonly channels: number;\n readonly subscriptions: number;\n readonly published: number;\n readonly delivered: number;\n readonly refused: number;\n readonly slowConsumers: number;\n}\n\n/** One connection, as the transport glue drives it. @since 1.0.0 */\nexport interface HubConnection {\n readonly id: string;\n readonly identity: Identity;\n /** Hands the hub one text frame from the peer. Frames are handled in order. */\n receive(text: string): void;\n /** Tells the hub the transport closed. Safe to call twice. */\n closed(): void;\n /** Resolves once every frame received so far was handled and every request answered. */\n settled(): Promise<void>;\n /** Resolves when the hub finished releasing what the connection held. */\n readonly done: Promise<void>;\n}\n\n/** How a connection differs from a long-lived one. @since 1.0.0 */\nexport interface ConnectOptions {\n /**\n * A connection that exists for one HTTP request: a request is answered for any identity the channel's `subscribe` rule\n * accepts (there is no subscription to look up), and nothing is kept after it closes.\n */\n readonly stateless?: boolean;\n /** Replaces the connection's own publish bucket, so a caller can share one across requests. */\n readonly publishBucket?: TokenBucket;\n /**\n * The id of a long-lived connection of the same user this request acts for (SSE clients post under their stream's id, so\n * `echo:false` and `publisher` work). Honored only when that connection is open on this server and has the same user.\n */\n readonly actingFor?: string;\n}\n\n/** Who to remove subscriptions of. At least one field. @since 1.0.0 */\nexport interface RevokeTarget {\n readonly user?: string;\n readonly connection?: string;\n readonly channel?: string;\n}\n\n/** The server object. @since 1.0.0 */\nexport interface HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: ChannelRule): HubCore;\n /** Starts handling a connection. Sends `ready`. */\n connect(transport: Transport, identity?: Identity, options?: ConnectOptions): HubConnection;\n /** Publishes from the server (no rule applies). */\n publish(\n channel: string,\n event: string,\n data: unknown,\n options?: { readonly key?: string },\n ): Promise<PublishResult>;\n /** Removes matching subscriptions; each subscriber gets `unsubbed why:\"revoked\"`. Returns how many were removed. */\n revoke(target: RevokeTarget): number;\n /** Counters. */\n stats(): HubStats;\n /** Closes every connection with 1001, stops timers, removes presence, settles requests. */\n close(): Promise<void>;\n /** @internal Changes the state of a presence member from outside its connection (SSE clients). */\n setMemberState(\n identity: Identity,\n channel: string,\n connectionId: string,\n state: Json,\n ): Promise<void>;\n /** @internal The resolved limits. */\n readonly limits: HubLimits;\n /** @internal The adapter. */\n readonly adapter: Adapter;\n /** @internal Reports an error. */\n report(error: unknown, context: ErrorContext): void;\n /** @internal Matches a channel against the rules. */\n match(\n channel: string,\n ): { readonly rule: ChannelRule; readonly params: Record<string, string> } | undefined;\n /** @internal Evaluates a rule permission for an identity. */\n allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean>;\n}\n\ninterface SubState {\n readonly conn: ConnState;\n readonly channel: ChanState;\n readonly rule: ChannelRule;\n readonly params: Record<string, string>;\n readonly echo: boolean;\n readonly watch: boolean;\n joined: boolean;\n version: number;\n /** The state this connection last published to the presence set, to write it back if the entry expired. */\n state: Json;\n lastSeq: number;\n /** The `at` of the last message delivered: a message with an old seq but a newer `at` means the numbering restarted. */\n lastAt: number;\n /** While true, live messages wait in `buffer` and `pump` delivers in order. */\n pumping: boolean;\n gapOk: boolean;\n buffer: StoredMessage[];\n presenceQueue: PresenceEvent[] | undefined;\n /** The join in progress, so that releasing the subscription waits for it and never leaves a member behind. */\n joining: Promise<void> | undefined;\n active: boolean;\n}\n\ninterface ChanState {\n readonly name: string;\n readonly subs: Set<SubState>;\n readonly bucket: TokenBucket;\n adapterSub: Promise<Unsubscribe> | undefined;\n presenceSub: Promise<Unsubscribe> | undefined;\n closing: boolean;\n /** Cache of the encoded frame of the message being fanned out. */\n frame: { readonly seq: number; readonly text: string } | undefined;\n}\n\ninterface ConnState {\n readonly id: string;\n readonly identity: Identity;\n readonly transport: Transport;\n readonly subs: Map<string, SubState>;\n readonly queue: string[];\n readonly publishBucket: TokenBucket;\n /** Spent by `sub` and `unsub` frames: subscription churn costs the adapter (and Redis) work like a publish does. */\n readonly controlBucket: TokenBucket;\n readonly invalid: WindowCounter;\n readonly limited: WindowCounter;\n readonly pending: Set<AbortController>;\n readonly stateless: boolean;\n readonly waiters: (() => void)[];\n running: boolean;\n closed: boolean;\n release: (() => void) | undefined;\n readonly done: Promise<void>;\n}\n\nconst REPLAY_PAGE = 500;\nconst MAX_PUMP_BUFFER = 10_000;\nconst ID_BYTES = 16;\n\n/** A random id: 128 bits, base64url. */\nfunction randomId(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(ID_BYTES));\n return btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction encodeFrame(frame: Record<string, unknown>): string {\n return JSON.stringify(frame);\n}\n\nfunction messageFrame(channel: string, message: StoredMessage, replayed: boolean): string {\n return (\n `{\"t\":\"msg\",\"c\":${JSON.stringify(channel)},\"s\":${message.seq},\"e\":${JSON.stringify(message.event)},\"d\":${message.data}` +\n (message.publisher === undefined ? \"\" : `,\"p\":${JSON.stringify(message.publisher)}`) +\n (message.user === undefined ? \"\" : `,\"u\":${JSON.stringify(message.user)}`) +\n (replayed ? ',\"r\":1}' : \"}\")\n );\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules, enforces limits and talks to\n * one adapter. Wire it to a transport with `hub.websocket()`, `hub.sse()` and `hub.post()` (Dusk routes) or with `hub.connect`.\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: { \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined } },\n * });\n * const sent: string[] = [];\n * const connection = hub.connect({ send: (frame) => (sent.push(frame), true), close: () => {} });\n * connection.receive('{\"t\":\"sub\",\"n\":1,\"c\":\"chat:lobby\"}');\n * @since 1.0.0\n */\nexport function createHubCore(options: HubOptions = {}): HubCore {\n const limits = resolveLimits(options.limits);\n const now = options.now ?? Date.now;\n const ownsAdapter = options.adapter === undefined;\n const adapter =\n options.adapter ??\n memoryAdapter({\n now,\n onError: (error, context) =>\n report(error, { where: `memory.${context.where}`, channel: context.channel }),\n });\n const rules: CompiledRule[] = [];\n const conns = new Map<string, ConnState>();\n const chans = new Map<string, ChanState>();\n let heartbeat: ReturnType<typeof setInterval> | undefined;\n let closing = false;\n let beating = false;\n const counters = { published: 0, delivered: 0, refused: 0, slowConsumers: 0 };\n\n const report = (error: unknown, context: ErrorContext): void => {\n if (options.onError !== undefined) {\n try {\n options.onError(error, context);\n } catch {\n // A failing reporter must not break the hub.\n }\n } else {\n console.error(`[realtime] ${context.where}`, error);\n }\n };\n\n function match(\n channel: string,\n ): { rule: ChannelRule; params: Record<string, string> } | undefined {\n for (const compiled of rules) {\n const params = matchRule(compiled, channel);\n if (params !== undefined) return { rule: compiled.rule, params };\n }\n return undefined;\n }\n\n async function allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean> {\n const permission = rule[op];\n if (permission === undefined || permission === false) return false;\n if (permission === true) return true;\n try {\n return (await permission({ channel, params, identity, op })) === true;\n } catch (error) {\n report(error, { where: `rule.${op}`, channel });\n throw new RealtimeError(\n \"unavailable\",\n __DEV__ ? \"the channel rule could not be evaluated\" : undefined,\n );\n }\n }\n\n function send(conn: ConnState, text: string): void {\n if (conn.closed) return;\n try {\n const buffered = conn.transport.buffered?.() ?? 0;\n if (buffered > limits.maxBufferedBytes) {\n counters.slowConsumers += 1;\n drop(conn, 1013, __DEV__ ? \"slow consumer\" : \"\");\n return;\n }\n if (conn.transport.send(text)) return;\n } catch (error) {\n // A transport that throws is a broken socket: this connection goes, the others are not affected (guarantee G13). The\n // transport is closed too, or its peer would keep a socket the hub no longer answers on until its own heartbeat gave up.\n report(error, { where: \"transport.send\", connection: conn.id });\n onTransport(conn, \"transport.close\", () =>\n conn.transport.close(1011, __DEV__ ? \"write failed\" : \"\"),\n );\n }\n void closeConnection(conn);\n }\n\n /** Runs a call on the transport; a transport that is already gone throws, which is only reported. */\n function onTransport(conn: ConnState, where: string, call: () => unknown): void {\n try {\n call();\n } catch (error) {\n report(error, { where, connection: conn.id });\n }\n }\n\n function sendFrame(conn: ConnState, frame: Record<string, unknown>): void {\n send(conn, encodeFrame(frame));\n }\n\n function fail(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n channel?: string,\n message?: string,\n ): void {\n counters.refused += 1;\n sendFrame(conn, {\n t: \"err\",\n ...(n === undefined ? {} : { n }),\n ...(channel === undefined ? {} : { c: channel }),\n code,\n ...(message === undefined || message === \"\" ? {} : { msg: message }),\n });\n }\n\n /** Sends a final frame, closes the transport and releases the connection. */\n function drop(conn: ConnState, code: number, reason: string): void {\n if (conn.closed) return;\n // The connection is released whatever the transport does, so its failures are only reported.\n onTransport(conn, \"transport.send\", () =>\n conn.transport.send(encodeFrame({ t: \"bye\", code, why: reason })),\n );\n onTransport(conn, \"transport.close\", () => conn.transport.close(code, reason));\n void closeConnection(conn);\n }\n\n // --- channels ------------------------------------------------------------------------------------------------------\n\n function channelState(name: string): ChanState {\n let chan = chans.get(name);\n if (chan === undefined || chan.closing) {\n chan = {\n name,\n subs: new Set(),\n bucket: new TokenBucket(\n limits.channelPublishesPerSecond,\n limits.channelPublishesPerSecond,\n now(),\n ),\n adapterSub: undefined,\n presenceSub: undefined,\n closing: false,\n frame: undefined,\n };\n chans.set(name, chan);\n }\n return chan;\n }\n\n /** Opens one of the channel's two adapter feeds once; a failed attempt is forgotten so the next caller retries. */\n async function ensureFeed(\n chan: ChanState,\n key: \"adapterSub\" | \"presenceSub\",\n open: () => Promise<Unsubscribe>,\n ): Promise<void> {\n if (chan[key] === undefined) {\n const opened = open();\n chan[key] = opened;\n opened.catch(() => {\n chan[key] = undefined;\n });\n }\n await chan[key];\n }\n\n function onMessage(chan: ChanState, message: StoredMessage): void {\n for (const sub of chan.subs) {\n if (!sub.active) continue;\n if (sub.pumping) {\n // A consumer so far behind that it cannot catch up is resubscribed from its last seq instead of holding memory.\n if (sub.buffer.length >= MAX_PUMP_BUFFER) removeSub(sub, \"gap\");\n else sub.buffer.push(message);\n continue;\n }\n if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);\n else if (message.seq > sub.lastSeq + 1) {\n sub.buffer.push(message);\n void pump(sub, undefined);\n } else if (message.at > sub.lastAt) removeSub(sub, \"reset\");\n // Any other seq at or below the cursor was already delivered: dropped (guarantee G2).\n }\n }\n\n function deliver(sub: SubState, message: StoredMessage, replayed: boolean): void {\n sub.lastSeq = message.seq;\n sub.lastAt = Math.max(sub.lastAt, message.at);\n if (!sub.echo && message.publisher === sub.conn.id) return;\n counters.delivered += 1;\n if (replayed) {\n send(sub.conn, messageFrame(sub.channel.name, message, true));\n return;\n }\n const cached = sub.channel.frame;\n const text =\n cached !== undefined && cached.seq === message.seq\n ? cached.text\n : messageFrame(sub.channel.name, message, false);\n if (cached === undefined || cached.seq !== message.seq)\n sub.channel.frame = { seq: message.seq, text };\n send(sub.conn, text);\n }\n\n /**\n * Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the\n * missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one\n * is removed with `unsubbed why:\"gap\"`.\n */\n async function pump(sub: SubState, upTo: number | undefined): Promise<void> {\n if (sub.pumping && upTo === undefined) return;\n sub.pumping = true;\n try {\n while (sub.active) {\n sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);\n const next = sub.buffer[0];\n if (next !== undefined && next.seq === sub.lastSeq + 1) {\n deliver(sub, next, false);\n sub.buffer.shift();\n continue;\n }\n const target = next === undefined ? (upTo ?? 0) : next.seq - 1;\n if (sub.lastSeq >= target) break;\n const page = await adapter.history(\n sub.channel.name,\n sub.lastSeq,\n Math.min(REPLAY_PAGE, target - sub.lastSeq),\n );\n if (!sub.active) return;\n const first = page.messages[0];\n if (first === undefined || first.seq !== sub.lastSeq + 1) {\n if (!sub.gapOk) {\n removeSub(sub, \"gap\");\n return;\n }\n // The gap was reported at subscribe time: continue from what is retained.\n sub.lastSeq = (first?.seq ?? target + 1) - 1;\n continue;\n }\n for (const message of page.messages) {\n if (message.seq > target) break;\n deliver(sub, message, true);\n }\n }\n } catch (error) {\n report(error, { where: \"pump\", channel: sub.channel.name, connection: sub.conn.id });\n if (sub.active) removeSub(sub, \"unavailable\");\n } finally {\n sub.pumping = false;\n }\n // A message that arrived while the last history page was awaited is in the buffer; one more pass delivers it.\n if (sub.active && sub.buffer.length > 0) void pump(sub, undefined);\n }\n\n function onPresence(chan: ChanState, event: PresenceEvent): void {\n for (const sub of chan.subs) {\n // A change to this connection's own member made elsewhere (an HTTP post to another server) is what a write-back after an\n // expiry has to carry, so the connection's copy follows it.\n if (\n sub.joined &&\n event.op !== \"leave\" &&\n event.member.id === sub.conn.id &&\n event.member.v > sub.version\n ) {\n sub.version = event.member.v;\n sub.state = event.member.s;\n }\n if (!sub.active || (!sub.joined && !sub.watch)) continue;\n if (sub.presenceQueue !== undefined) sub.presenceQueue.push(event);\n else sendPresence(sub, event);\n }\n }\n\n function sendPresence(sub: SubState, event: PresenceEvent): void {\n sendFrame(sub.conn, { t: \"pres\", c: event.channel, op: event.op, m: event.member });\n }\n\n /** Removes a subscription from its connection and channel; tells the peer when `why` is given. */\n function removeSub(sub: SubState, why?: string): void {\n if (!sub.active) return;\n sub.active = false;\n sub.conn.subs.delete(sub.channel.name);\n sub.channel.subs.delete(sub);\n if (why !== undefined && !sub.conn.closed)\n sendFrame(sub.conn, { t: \"unsubbed\", c: sub.channel.name, ...(why === \"\" ? {} : { why }) });\n void releaseSub(sub);\n }\n\n /** The releases still running, so that `close` finishes them before it closes the adapter. */\n const releasing = new Set<Promise<void>>();\n\n function releaseSub(sub: SubState): Promise<void> {\n const running = release(sub);\n releasing.add(running);\n void running.finally(() => releasing.delete(running));\n return running;\n }\n\n async function release(sub: SubState): Promise<void> {\n const chan = sub.channel;\n try {\n // A join that is still being written must finish first, or its member would outlive the connection.\n await sub.joining?.catch(() => undefined);\n if (sub.joined) {\n sub.joined = false;\n await adapter.presenceRemove(chan.name, sub.conn.id);\n }\n } catch (error) {\n report(error, { where: \"presenceRemove\", channel: chan.name, connection: sub.conn.id });\n }\n if (chan.subs.size > 0 || chan.closing) return;\n chan.closing = true;\n if (chans.get(chan.name) === chan) chans.delete(chan.name);\n const stops = [chan.adapterSub, chan.presenceSub].filter(\n (item): item is Promise<Unsubscribe> => item !== undefined,\n );\n await Promise.all(\n stops.map(async (stop) => {\n try {\n await (await stop)();\n } catch (error) {\n report(error, { where: \"unsubscribe\", channel: chan.name });\n }\n }),\n );\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n function identityDenied(conn: ConnState): RealtimeError[\"code\"] {\n return conn.identity.user === undefined ? \"unauthorized\" : \"forbidden\";\n }\n\n async function subscribe(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"sub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n if (conn.subs.has(name))\n return fail(conn, n, \"conflict\", name, __DEV__ ? \"already subscribed\" : undefined);\n if (conn.subs.size >= limits.maxChannelsPerConnection)\n return fail(\n conn,\n n,\n \"limit\",\n name,\n __DEV__ ? \"too many channels on this connection\" : undefined,\n );\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n const wantsPresence = frame.hasPresence || frame.watch === true;\n if (\n wantsPresence &&\n !(await allowed(found.rule, \"presence\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (\n frame.hasPresence &&\n byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes\n )\n return fail(\n conn,\n n,\n \"too_large\",\n name,\n __DEV__ ? \"the presence state is too large\" : undefined,\n );\n if (conn.closed) return;\n const chan = channelState(name);\n if (chan.subs.size >= limits.maxSubscribersPerChannel)\n return fail(conn, n, \"limit\", name, __DEV__ ? \"the channel is full\" : undefined);\n const sub: SubState = {\n conn,\n channel: chan,\n rule: found.rule,\n params: found.params,\n echo: frame.echo !== false,\n watch: frame.watch === true,\n joined: false,\n version: 0,\n state: null,\n lastSeq: 0,\n lastAt: now(),\n pumping: true,\n gapOk: false,\n buffer: [],\n presenceQueue: wantsPresence ? [] : undefined,\n joining: undefined,\n active: true,\n };\n conn.subs.set(name, sub);\n chan.subs.add(sub);\n try {\n await ensureFeed(chan, \"adapterSub\", () =>\n adapter.subscribe(chan.name, (message) => onMessage(chan, message)),\n );\n if (wantsPresence) {\n await ensureFeed(chan, \"presenceSub\", () =>\n adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)),\n );\n startHeartbeat();\n }\n const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);\n if (!sub.active) return;\n const head = probe.head;\n const after = frame.after;\n let gap = false;\n let reset = false;\n if (after === undefined) {\n sub.lastSeq = head;\n } else if (after > head) {\n reset = true;\n sub.lastSeq = head;\n } else {\n sub.lastSeq = after;\n gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;\n sub.gapOk = gap;\n }\n sendFrame(conn, {\n t: \"subbed\",\n n,\n c: name,\n head,\n ...(gap ? { gap: true } : {}),\n ...(reset ? { reset: true } : {}),\n });\n if (frame.hasPresence) {\n sub.joining = joinPresence(sub, frame.presence ?? null);\n await sub.joining;\n }\n if (wantsPresence) await syncPresence(sub);\n if (!sub.active) return;\n await pump(sub, head);\n sub.gapOk = false;\n } catch (error) {\n if (sub.active) {\n removeSub(sub);\n report(error, { where: \"subscribe\", channel: name, connection: conn.id });\n fail(conn, n, error instanceof RealtimeError ? error.code : \"unavailable\", name);\n }\n }\n }\n\n async function joinPresence(sub: SubState, state: Json): Promise<void> {\n const existing = await adapter.presenceList(sub.channel.name);\n if (existing.length >= limits.maxPresenceMembers)\n throw new RealtimeError(\"limit\", __DEV__ ? \"the presence set is full\" : undefined);\n sub.version = 1;\n sub.state = state;\n sub.joined = true;\n startHeartbeat();\n const member: Member = {\n id: sub.conn.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: state,\n v: 1,\n };\n await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);\n }\n\n async function syncPresence(sub: SubState): Promise<void> {\n const members = await adapter.presenceList(sub.channel.name);\n if (!sub.active) return;\n sendFrame(sub.conn, { t: \"pres\", c: sub.channel.name, op: \"sync\", m: members });\n const queued = sub.presenceQueue ?? [];\n sub.presenceQueue = undefined;\n const known = new Map(members.map((member) => [member.id, member.v]));\n for (const event of queued) {\n // What the snapshot already shows is not announced again.\n const version = known.get(event.member.id);\n if (\n event.op === \"leave\"\n ? version === undefined\n : version !== undefined && version >= event.member.v\n )\n continue;\n sendPresence(sub, event);\n }\n }\n\n async function setPresence(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pres\" }>,\n ): Promise<void> {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined || !sub.joined) return fail(conn, undefined, \"not_subscribed\", frame.c);\n if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes)\n return fail(\n conn,\n undefined,\n \"too_large\",\n frame.c,\n __DEV__ ? \"the presence state is too large\" : undefined,\n );\n if (!conn.publishBucket.take(now())) return limit(conn, undefined, frame.c);\n sub.version += 1;\n sub.state = frame.s;\n const member: Member = {\n id: conn.id,\n ...(conn.identity.user === undefined ? {} : { u: conn.identity.user }),\n s: frame.s,\n v: sub.version,\n };\n try {\n await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);\n } catch (error) {\n report(error, { where: \"presenceSet\", channel: frame.c, connection: conn.id });\n fail(conn, undefined, \"unavailable\", frame.c);\n }\n }\n\n function limit(conn: ConnState, n: number | undefined, channel: string): void {\n fail(conn, n, \"rate_limited\", channel);\n if (conn.limited.hit(now())) drop(conn, 1008, __DEV__ ? \"rate limit exceeded\" : \"\");\n }\n\n async function publishFrame(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"publish\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (frame.d.length > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, __DEV__ ? \"the message is too large\" : undefined);\n // A channel with no subscriber on this server has no bucket: only the connection's own rate applies to it.\n if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false)\n return limit(conn, n, name);\n try {\n const result = await adapter.publish(\n name,\n {\n event: frame.e,\n data: frame.d,\n publisher: conn.id,\n ...(conn.identity.user === undefined ? {} : { user: conn.identity.user }),\n },\n {\n history: found.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(frame.k === undefined ? {} : { key: frame.k }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n if (n !== undefined)\n sendFrame(conn, { t: \"ack\", n, s: result.seq, ...(result.duplicate ? { dup: true } : {}) });\n } catch (error) {\n report(error, { where: \"publish\", channel: name, connection: conn.id });\n fail(conn, n, \"unavailable\", name);\n }\n }\n\n async function request(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"req\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n let target: { rule: ChannelRule; params: Record<string, string> } | undefined =\n conn.subs.get(name);\n if (target === undefined && conn.stateless) {\n const found = match(name);\n if (\n found !== undefined &&\n (await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n target = found;\n }\n if (target === undefined)\n return fail(\n conn,\n n,\n identityDenied(conn) === \"unauthorized\" && conn.stateless\n ? \"unauthorized\"\n : \"not_subscribed\",\n name,\n );\n const sub = target;\n const handler = sub.rule.handlers?.[frame.e];\n if (handler === undefined) return fail(conn, n, \"no_handler\", name);\n if (conn.pending.size >= limits.maxPendingRequests)\n return fail(conn, n, \"limit\", name, __DEV__ ? \"too many requests in flight\" : undefined);\n if (!conn.publishBucket.take(now())) return limit(conn, n, name);\n const controller = new AbortController();\n conn.pending.add(controller);\n const timeout = Math.min(frame.ms ?? limits.requestTimeoutMs, limits.requestTimeoutMs);\n const timer = setTimeout(() => controller.abort(new RealtimeError(\"timeout\")), timeout);\n void (async () => {\n try {\n const run = Promise.resolve(\n handler({\n channel: name,\n params: sub.params,\n identity: conn.identity,\n event: frame.e,\n data: frame.d,\n signal: controller.signal,\n }),\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n controller.signal.addEventListener(\"abort\", () => reject(controller.signal.reason), {\n once: true,\n });\n });\n const reply = (await Promise.race([run, aborted])) ?? null;\n const text = JSON.stringify(reply);\n if (text === undefined || byteLength(text) > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, __DEV__ ? \"the reply is too large\" : undefined);\n send(conn, `{\"t\":\"rep\",\"n\":${n},\"d\":${text}}`);\n } catch (error) {\n if (error instanceof RealtimeError)\n fail(\n conn,\n n,\n error.code,\n name,\n error.code === \"timeout\" || error.code === \"closed\" ? undefined : error.message,\n );\n else {\n report(error, { where: \"request\", channel: name, connection: conn.id });\n fail(conn, n, \"handler_error\", name);\n }\n } finally {\n clearTimeout(timer);\n conn.pending.delete(controller);\n settle(conn);\n }\n })();\n }\n\n /** Answers a frame that cannot be handled; a peer that keeps sending them is dropped. */\n function refuseFrame(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n why?: string,\n ): void {\n fail(conn, n, code, undefined, why);\n if (conn.invalid.hit(now())) drop(conn, 1008, __DEV__ ? \"too many invalid frames\" : \"\");\n }\n\n async function handle(conn: ConnState, text: string): Promise<void> {\n const decoded = decodeClientFrame(text, limits.maxMessageBytes);\n if (!decoded.ok) {\n refuseFrame(conn, decoded.n, decoded.code, decoded.why);\n return;\n }\n const frame = decoded.frame;\n const n = \"n\" in frame ? frame.n : undefined;\n // An `unsub` spends a token but is never refused: the client cannot tell a refused one (the answer names no frame), and would\n // believe it left a channel the hub still holds, so its next `sub` of that channel would be a conflict.\n if (\n (frame.t === \"sub\" || frame.t === \"unsub\") &&\n !conn.controlBucket.take(now()) &&\n frame.t === \"sub\"\n )\n return limit(conn, n, frame.c);\n try {\n switch (frame.t) {\n case \"hello\":\n return;\n case \"ping\":\n sendFrame(conn, { t: \"pong\", ...(frame.n === undefined ? {} : { n: frame.n }) });\n return;\n case \"sub\":\n return await subscribe(conn, frame);\n case \"unsub\": {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined) return fail(conn, frame.n, \"not_subscribed\", frame.c);\n removeSub(sub);\n return;\n }\n case \"pub\":\n return await publishFrame(conn, frame);\n case \"req\":\n return await request(conn, frame);\n case \"pres\":\n return await setPresence(conn, frame);\n }\n } catch (error) {\n const channel = \"c\" in frame ? frame.c : undefined;\n if (error instanceof RealtimeError) fail(conn, n, error.code, channel);\n else {\n report(error, {\n where: \"frame\",\n connection: conn.id,\n ...(channel === undefined ? {} : { channel }),\n });\n fail(conn, n, \"unavailable\", channel);\n }\n }\n }\n\n async function run(conn: ConnState): Promise<void> {\n if (conn.running) return;\n conn.running = true;\n try {\n for (let text = conn.queue.shift(); text !== undefined; text = conn.queue.shift()) {\n if (conn.closed) break;\n try {\n await handle(conn, text);\n } catch (error) {\n if (error instanceof RealtimeError) fail(conn, undefined, error.code);\n else {\n report(error, { where: \"frame\", connection: conn.id });\n fail(conn, undefined, \"unavailable\");\n }\n }\n }\n } finally {\n conn.running = false;\n settle(conn);\n }\n }\n\n /** Wakes the callers of `settled` when the connection has nothing left to do. */\n function settle(conn: ConnState): void {\n if (conn.running || conn.queue.length > 0 || conn.pending.size > 0) return;\n for (const wake of conn.waiters.splice(0)) wake();\n }\n\n async function closeConnection(conn: ConnState): Promise<void> {\n if (conn.closed) return conn.done;\n conn.closed = true;\n if (conns.get(conn.id) === conn) conns.delete(conn.id);\n for (const controller of conn.pending) controller.abort(new RealtimeError(\"closed\"));\n conn.pending.clear();\n conn.queue.length = 0;\n for (const wake of conn.waiters.splice(0)) wake();\n const subs = [...conn.subs.values()];\n for (const sub of subs) {\n sub.active = false;\n sub.channel.subs.delete(sub);\n }\n conn.subs.clear();\n await Promise.allSettled(subs.map((sub) => releaseSub(sub)));\n conn.release?.();\n }\n\n // --- presence heartbeat --------------------------------------------------------------------------------------------\n\n function startHeartbeat(): void {\n if (heartbeat !== undefined || closing) return;\n const timer = setInterval(() => void beat(), limits.presenceHeartbeatMs);\n // Node's timer can be unref'd so an idle hub does not keep the process alive; other runtimes return a number.\n (timer as unknown as { unref?: () => void }).unref?.();\n heartbeat = timer;\n }\n\n async function beat(): Promise<void> {\n if (beating) return;\n beating = true;\n try {\n await beatOnce();\n } finally {\n beating = false;\n }\n }\n\n async function beatOnce(): Promise<void> {\n const refs: { channel: string; id: string }[] = [];\n const watched = new Set<string>();\n for (const conn of conns.values())\n for (const sub of conn.subs.values()) {\n if (sub.joined) refs.push({ channel: sub.channel.name, id: conn.id });\n if (sub.joined || sub.watch) watched.add(sub.channel.name);\n }\n if (refs.length === 0 && watched.size === 0) {\n clearInterval(heartbeat);\n heartbeat = undefined;\n return;\n }\n try {\n const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);\n for (const ref of missing) {\n const sub = conns.get(ref.id)?.subs.get(ref.channel);\n if (sub === undefined || !sub.joined) continue;\n await adapter.presenceSet(\n ref.channel,\n {\n id: ref.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: sub.state,\n v: sub.version,\n },\n limits.presenceTtlMs,\n );\n }\n for (const channel of watched) await adapter.presenceReap(channel);\n } catch (error) {\n report(error, { where: \"presenceHeartbeat\" });\n }\n }\n\n // --- the hub -------------------------------------------------------------------------------------------------------\n\n for (const [pattern, rule] of Object.entries(options.channels ?? {}))\n rules.push(compileRule(pattern, rule));\n\n const core: HubCore = {\n limits,\n adapter,\n report,\n match,\n allowed,\n\n channel(pattern, rule) {\n rules.push(compileRule(pattern, rule));\n return core;\n },\n\n connect(transport, identity = {}, connectOptions = {}) {\n if (closing) throw new RealtimeError(\"closed\", __DEV__ ? \"the hub is closed\" : undefined);\n let release: () => void = () => undefined;\n const done = new Promise<void>((resolve) => {\n release = resolve;\n });\n const owner =\n connectOptions.actingFor === undefined ? undefined : conns.get(connectOptions.actingFor);\n const acting =\n owner !== undefined && identity.user !== undefined && owner.identity.user === identity.user;\n const conn: ConnState = {\n id: acting ? (connectOptions.actingFor as string) : randomId(),\n identity,\n transport,\n subs: new Map(),\n queue: [],\n publishBucket:\n connectOptions.publishBucket ??\n new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),\n // The refill is the publish rate; the burst always covers a connection declaring all its channels at once.\n controlBucket: new TokenBucket(\n limits.publishesPerSecond,\n Math.max(limits.publishBurst, limits.maxChannelsPerConnection),\n now(),\n ),\n stateless: connectOptions.stateless === true,\n waiters: [],\n invalid: new WindowCounter(10, 60_000),\n limited: new WindowCounter(50, 10_000),\n pending: new Set(),\n running: false,\n closed: false,\n release,\n done,\n };\n // An acting connection shares its owner's id: it must not replace the owner in the table.\n if (!acting) conns.set(conn.id, conn);\n sendFrame(conn, {\n t: \"ready\",\n v: PROTOCOL_VERSION,\n id: conn.id,\n hb: limits.heartbeatMs,\n max: limits.maxMessageBytes,\n });\n return {\n id: conn.id,\n identity,\n done,\n receive(text) {\n if (conn.closed) return;\n // Judged before it takes a place in the queue: a frame over the limit can never be handled, so it must not be able to\n // fill the queue (or be held until its turn) however many arrive.\n if (byteLength(text) > limits.maxMessageBytes) {\n refuseFrame(conn, undefined, \"too_large\");\n return;\n }\n if (conn.queue.length >= limits.maxQueuedFrames) {\n drop(conn, 1013, __DEV__ ? \"too many frames waiting\" : \"\");\n return;\n }\n conn.queue.push(text);\n void run(conn);\n },\n closed() {\n void closeConnection(conn);\n },\n settled() {\n return new Promise<void>((resolve) => {\n conn.waiters.push(resolve);\n settle(conn);\n });\n },\n };\n },\n\n async publish(channel, event, data, publishOptions = {}) {\n if (!isChannelName(channel))\n throw new RealtimeError(\n \"bad_channel\",\n __DEV__ ? `\"${channel}\" is not a valid channel name` : undefined,\n );\n if (!isEventName(event))\n throw new RealtimeError(\n \"bad_frame\",\n __DEV__ ? `\"${event}\" is not a valid event name` : undefined,\n );\n if (publishOptions.key !== undefined && !isIdempotencyKey(publishOptions.key))\n throw new RealtimeError(\n \"bad_frame\",\n __DEV__ ? \"the idempotency key is not valid\" : undefined,\n );\n let text: string | undefined;\n try {\n text = JSON.stringify(data === undefined ? null : data);\n } catch (error) {\n throw new TypeError(__DEV__ ? \"the data is not JSON\" : \"REALTIME_DATA\", {\n cause: error,\n });\n }\n if (text === undefined)\n throw new TypeError(__DEV__ ? \"the data is not JSON\" : \"REALTIME_DATA\");\n if (byteLength(text) > limits.maxMessageBytes)\n throw new RealtimeError(\"too_large\", __DEV__ ? \"the message is too large\" : undefined);\n const found = match(channel);\n const result = await adapter.publish(\n channel,\n { event, data: text },\n {\n history: found?.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(publishOptions.key === undefined ? {} : { key: publishOptions.key }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n return result;\n },\n\n async setMemberState(identity, channel, connectionId, state) {\n if (identity.user === undefined)\n throw new RealtimeError(\n \"unauthorized\",\n __DEV__ ? \"presence changes over HTTP need a signed-in user\" : undefined,\n );\n const found = match(channel);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"presence\", channel, found.params, identity))\n )\n throw new RealtimeError(\"forbidden\");\n if (byteLength(JSON.stringify(state)) > limits.maxStateBytes)\n throw new RealtimeError(\n \"too_large\",\n __DEV__ ? \"the presence state is too large\" : undefined,\n );\n const members = await adapter.presenceList(channel);\n const member = members.find((item) => item.id === connectionId);\n if (member === undefined || member.u !== identity.user)\n throw new RealtimeError(\"forbidden\", __DEV__ ? \"that connection is not yours\" : undefined);\n // The connection may live on this server: its own copy of the member is what the heartbeat writes back after an expiry, so\n // it moves with the stored one (the same for a connection on another server, through the presence event below).\n const local = conns.get(connectionId)?.subs.get(channel);\n const version = Math.max(member.v, local?.version ?? 0) + 1;\n if (local?.joined === true) {\n local.version = version;\n local.state = state;\n }\n await adapter.presenceSet(\n channel,\n { id: member.id, u: member.u, s: state, v: version },\n limits.presenceTtlMs,\n );\n },\n\n revoke(target) {\n if (\n target.user === undefined &&\n target.connection === undefined &&\n target.channel === undefined\n )\n throw new TypeError(\n __DEV__ ? \"revoke needs a user, a connection or a channel.\" : \"REALTIME_REVOKE\",\n );\n let count = 0;\n for (const conn of [...conns.values()]) {\n if (target.connection !== undefined && conn.id !== target.connection) continue;\n if (target.user !== undefined && conn.identity.user !== target.user) continue;\n for (const sub of [...conn.subs.values()]) {\n if (target.channel !== undefined && sub.channel.name !== target.channel) continue;\n removeSub(sub, \"revoked\");\n count += 1;\n }\n }\n return count;\n },\n\n stats() {\n let subscriptions = 0;\n for (const conn of conns.values()) subscriptions += conn.subs.size;\n return {\n connections: conns.size,\n channels: chans.size,\n subscriptions,\n ...counters,\n };\n },\n\n async close() {\n if (closing) return;\n closing = true;\n if (heartbeat !== undefined) clearInterval(heartbeat);\n heartbeat = undefined;\n const all = [...conns.values()];\n for (const conn of all) drop(conn, 1001, __DEV__ ? \"going away\" : \"\");\n await Promise.all(all.map((conn) => conn.done));\n await Promise.allSettled([...releasing]);\n if (ownsAdapter) await adapter.close();\n },\n };\n return core;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createHub","core","createHubCore","options","hub","pattern","rule","websocketRoute","routeOptions","sseRoute","postRoute","DEFAULT_HUB_LIMITS","memoryAdapter"],"sources":["../../../src/server/index.ts"],"sourcesContent":["import {\n type HubRouteOptions,\n postRoute,\n type RequestLike,\n type SseLike,\n sseRoute,\n type WebSocketRoute,\n websocketRoute,\n} from \"./http\";\nimport { createHubCore, type HubCore, type HubOptions } from \"./hub\";\nimport type { Identity } from \"./rules\";\n\nexport type { Adapter } from \"../adapter\";\nexport { memoryAdapter } from \"../memory\";\nexport type {\n AuthenticateResult,\n HubRouteOptions,\n RequestLike,\n SseLike,\n WebSocketLike,\n WebSocketRoute,\n} from \"./http\";\nexport type {\n ConnectOptions,\n ErrorContext,\n HubConnection,\n HubOptions,\n HubStats,\n RevokeTarget,\n Transport,\n} from \"./hub\";\nexport type { HubLimits } from \"./limits\";\nexport { DEFAULT_HUB_LIMITS } from \"./limits\";\nexport type {\n ChannelRule,\n Identity,\n Permission,\n RequestContext,\n RequestHandler,\n RuleContext,\n} from \"./rules\";\n\n/**\n * The server object of `@shadow-js/realtime`: connections, channel rules, limits and an adapter, with the routes that\n * connect it to a transport.\n * @since 1.0.0\n */\nexport interface Hub extends HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub;\n /** The WebSocket route: `export const WS = hub.websocket()`. */\n websocket<E extends RequestLike = RequestLike>(\n options?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E>;\n /** The server-sent events route: `export const SSE = hub.sse()`. */\n sse<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E>,\n ): (event: E, stream: SseLike) => Promise<void>;\n /** The HTTP route for what SSE clients send: `export const POST = hub.post()`. */\n post<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string },\n ): (event: E) => Promise<Response>;\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules (default deny), enforces the\n * limits, and talks to one adapter (in memory by default; Redis or Cloudflare when several servers share channels).\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: {\n * \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },\n * },\n * });\n * await hub.publish(\"chat:lobby\", \"message\", { text: \"hello\" });\n * await hub.close();\n * @since 1.0.0\n */\nexport function createHub(options: HubOptions = {}): Hub {\n const core = createHubCore(options);\n const hub: Hub = Object.assign(core, {\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub {\n core.channel(pattern, rule);\n return hub;\n },\n websocket: <E extends RequestLike = RequestLike>(\n routeOptions?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E> => websocketRoute<E>(core, routeOptions),\n sse: <E extends RequestLike = RequestLike>(routeOptions?: HubRouteOptions<E>) =>\n sseRoute<E>(core, routeOptions),\n post: <E extends RequestLike = RequestLike>(\n routeOptions?: HubRouteOptions<E> & {\n readonly key?: (event: E, identity: Identity) => string;\n },\n ) => postRoute<E>(core, routeOptions),\n });\n return hub;\n}\n"],"mappings":"A,+MAmFA,SAAgBA,EAAU,EAAsB,CAAC,EAAQ,CACvD,IAAMC,EAAOC,EAAcC,CAAO,EAC5BC,EAAW,OAAO,OAAOH,EAAM,CACnC,QAAQ,EAAiB,EAA0C,CAEjE,OADAA,EAAK,QAAQI,EAASC,CAAI,EACnBF,CACT,EACA,UACE,GACsBG,EAAkBN,EAAMO,CAAY,EAC5D,IAA2C,GACzCC,EAAYR,EAAMO,CAAY,EAChC,KACE,GAGGE,EAAaT,EAAMO,CAAY,CACtC,CAAC,EACD,OAAOJ,CACT,C"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["createHub","core","createHubCore","options","hub","pattern","rule","websocketRoute","routeOptions","sseRoute","postRoute","DEFAULT_HUB_LIMITS","memoryAdapter"],"sources":["../../../src/server/index.ts"],"sourcesContent":["import {\n type HubRouteOptions,\n postRoute,\n type RequestLike,\n type SseLike,\n sseRoute,\n type WebSocketRoute,\n websocketRoute,\n} from \"./http\";\nimport { createHubCore, type HubCore, type HubOptions } from \"./hub\";\nimport type { Identity } from \"./rules\";\n\nexport type { Adapter } from \"../adapter\";\nexport { type MemoryAdapterOptions, type MemoryErrorContext, memoryAdapter } from \"../memory\";\nexport type {\n AuthenticateResult,\n HubRouteOptions,\n RequestLike,\n SseLike,\n WebSocketLike,\n WebSocketRoute,\n} from \"./http\";\nexport type {\n ConnectOptions,\n ErrorContext,\n HubConnection,\n HubOptions,\n HubStats,\n RevokeTarget,\n Transport,\n} from \"./hub\";\nexport type { HubLimits } from \"./limits\";\nexport { DEFAULT_HUB_LIMITS } from \"./limits\";\nexport type {\n ChannelRule,\n Identity,\n Permission,\n RequestContext,\n RequestHandler,\n RuleContext,\n} from \"./rules\";\n\n/**\n * The server object of `@shadow-js/realtime`: connections, channel rules, limits and an adapter, with the routes that\n * connect it to a transport.\n * @since 1.0.0\n */\nexport interface Hub extends HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub;\n /** The WebSocket route: `export const WS = hub.websocket()`. */\n websocket<E extends RequestLike = RequestLike>(\n options?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E>;\n /** The server-sent events route: `export const SSE = hub.sse()`. */\n sse<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E>,\n ): (event: E, stream: SseLike) => Promise<void>;\n /** The HTTP route for what SSE clients send: `export const POST = hub.post()`. */\n post<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string },\n ): (event: E) => Promise<Response>;\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules (default deny), enforces the\n * limits, and talks to one adapter (in memory by default; Redis or Cloudflare when several servers share channels).\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: {\n * \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },\n * },\n * });\n * await hub.publish(\"chat:lobby\", \"message\", { text: \"hello\" });\n * await hub.close();\n * @since 1.0.0\n */\nexport function createHub(options: HubOptions = {}): Hub {\n const core = createHubCore(options);\n const hub: Hub = Object.assign(core, {\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub {\n core.channel(pattern, rule);\n return hub;\n },\n websocket: <E extends RequestLike = RequestLike>(\n routeOptions?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E> => websocketRoute<E>(core, routeOptions),\n sse: <E extends RequestLike = RequestLike>(routeOptions?: HubRouteOptions<E>) =>\n sseRoute<E>(core, routeOptions),\n post: <E extends RequestLike = RequestLike>(\n routeOptions?: HubRouteOptions<E> & {\n readonly key?: (event: E, identity: Identity) => string;\n },\n ) => postRoute<E>(core, routeOptions),\n });\n return hub;\n}\n"],"mappings":"A,+MAmFA,SAAgBA,EAAU,EAAsB,CAAC,EAAQ,CACvD,IAAMC,EAAOC,EAAcC,CAAO,EAC5BC,EAAW,OAAO,OAAOH,EAAM,CACnC,QAAQ,EAAiB,EAA0C,CAEjE,OADAA,EAAK,QAAQI,EAASC,CAAI,EACnBF,CACT,EACA,UACE,GACsBG,EAAkBN,EAAMO,CAAY,EAC5D,IAA2C,GACzCC,EAAYR,EAAMO,CAAY,EAChC,KACE,GAGGE,EAAaT,EAAMO,CAAY,CACtC,CAAC,EACD,OAAOJ,CACT,C"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=Object.freeze({maxMessageBytes:65536,maxChannelsPerConnection:64,maxSubscribersPerChannel:5e4,maxPresenceMembers:1e3,maxStateBytes:1024,maxPendingRequests:32,maxQueuedFrames:256,maxBufferedBytes:1048576,publishesPerSecond:20,publishBurst:40,channelPublishesPerSecond:500,requestTimeoutMs:1e4,history:100,historyTtlMs:3e5,idempotencyTtlMs:6e4,presenceTtlMs:45e3,presenceHeartbeatMs:15e3,heartbeatMs:3e4}),t=1e4;function n(
|
|
1
|
+
const e=Object.freeze({maxMessageBytes:65536,maxChannelsPerConnection:64,maxSubscribersPerChannel:5e4,maxPresenceMembers:1e3,maxStateBytes:1024,maxPendingRequests:32,maxQueuedFrames:256,maxBufferedBytes:1048576,publishesPerSecond:20,publishBurst:40,channelPublishesPerSecond:500,requestTimeoutMs:1e4,history:100,historyTtlMs:3e5,idempotencyTtlMs:6e4,presenceTtlMs:45e3,presenceHeartbeatMs:15e3,heartbeatMs:3e4}),t=1e4;function n(t){let n={...e};for(let[r,i]of Object.entries(t??{})){if(!(r in e))throw TypeError(`REALTIME_LIMIT_2`);if(i===void 0)continue;let t=r===`history`?0:1;if(typeof i!=`number`||!Number.isInteger(i)||i<t)throw RangeError(`REALTIME_LIMIT_4`);n[r]=i}if(n.history>1e4)throw RangeError(`REALTIME_LIMIT_1`);if(n.presenceTtlMs<=n.presenceHeartbeatMs)throw RangeError(`REALTIME_LIMIT_3`);return n}var r=class{#e;#t;#n;#r;constructor(e,t,n){this.#n=e,this.#r=t,this.#e=t,this.#t=n}take(e){let t=Math.max(0,e-this.#t);return this.#t=e,this.#e=Math.min(this.#r,this.#e+t*this.#n/1e3),this.#e<1?!1:(--this.#e,!0)}},i=class{#e=[];#t;#n;constructor(e,t){this.#t=e,this.#n=t}hit(e){this.#e.push(e);let t=e-this.#n;for(;this.#e.length>0&&this.#e[0]<=t;)this.#e.shift();return this.#e.length>=this.#t}};export{e as DEFAULT_HUB_LIMITS,t as MAX_HISTORY,r as TokenBucket,i as WindowCounter,n as resolveLimits};
|
|
2
2
|
//# sourceMappingURL=limits.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limits.js","names":["DEFAULT_HUB_LIMITS","MAX_HISTORY","resolveLimits","out","name","value","given","floor","\"history\"","\"presenceTtlMs\"","\"presenceHeartbeatMs\"","TokenBucket","#rate","rate","#burst","burst","#tokens","#at","now","elapsed","WindowCounter","#limit","limit","#windowMs","windowMs","#times","cutoff"],"sources":["../../../src/server/limits.ts"],"sourcesContent":["/**\n * The limits of a hub. Every one is bounded, so a hub cannot be made to hold unbounded state (see the realtime protocol reference).\n * @since 1.0.0\n */\nexport interface HubLimits {\n /** Largest frame, in UTF-8 bytes, either way. Default 65,536. */\n readonly maxMessageBytes: number;\n /** Channels one connection may subscribe to. Default 64. */\n readonly maxChannelsPerConnection: number;\n /** Subscribers one channel may have on this server. Default 50,000. */\n readonly maxSubscribersPerChannel: number;\n /** Presence members one channel may have, across servers. Default 1,000. */\n readonly maxPresenceMembers: number;\n /** Largest presence state, in bytes of JSON. Default 1,024. */\n readonly maxStateBytes: number;\n /** Requests one connection may have in flight. Default 32. */\n readonly maxPendingRequests: number;\n /** Frames one connection may have waiting to be handled. Default 256. */\n readonly maxQueuedFrames: number;\n /** Bytes the transport may hold unsent before the connection is closed as a slow consumer. Default 1 MiB. */\n readonly maxBufferedBytes: number;\n
|
|
1
|
+
{"version":3,"file":"limits.js","names":["DEFAULT_HUB_LIMITS","MAX_HISTORY","resolveLimits","out","name","value","given","floor","\"history\"","\"presenceTtlMs\"","\"presenceHeartbeatMs\"","TokenBucket","#rate","rate","#burst","burst","#tokens","#at","now","elapsed","WindowCounter","#limit","limit","#windowMs","windowMs","#times","cutoff"],"sources":["../../../src/server/limits.ts"],"sourcesContent":["/**\n * The limits of a hub. Every one is bounded, so a hub cannot be made to hold unbounded state (see the realtime protocol reference).\n * @since 1.0.0\n */\nexport interface HubLimits {\n /** Largest frame, in UTF-8 bytes, either way. Default 65,536. */\n readonly maxMessageBytes: number;\n /** Channels one connection may subscribe to. Default 64. */\n readonly maxChannelsPerConnection: number;\n /** Subscribers one channel may have on this server. Default 50,000. */\n readonly maxSubscribersPerChannel: number;\n /** Presence members one channel may have, across servers. Default 1,000. */\n readonly maxPresenceMembers: number;\n /** Largest presence state, in bytes of JSON. Default 1,024. */\n readonly maxStateBytes: number;\n /** Requests one connection may have in flight. Default 32. */\n readonly maxPendingRequests: number;\n /** Frames one connection may have waiting to be handled. Default 256. */\n readonly maxQueuedFrames: number;\n /** Bytes the transport may hold unsent before the connection is closed as a slow consumer. Default 1 MiB. */\n readonly maxBufferedBytes: number;\n /**\n * Publishes per second per connection. Default 20. It is also the refill rate of the bucket that `sub` and `unsub` frames spend\n * from (burst: the larger of `publishBurst` and `maxChannelsPerConnection`), so subscription churn is limited like publishing.\n */\n readonly publishesPerSecond: number;\n /** Largest burst of publishes per connection. Default 40. */\n readonly publishBurst: number;\n /** Publishes per second per channel on this server. Default 500. */\n readonly channelPublishesPerSecond: number;\n /** Milliseconds a request may run. Default 10,000. */\n readonly requestTimeoutMs: number;\n /** Messages kept per channel unless a rule says otherwise. Default 100. */\n readonly history: number;\n /** Milliseconds a message is kept. Default 300,000. */\n readonly historyTtlMs: number;\n /** Milliseconds an idempotency key is remembered. Default 60,000. */\n readonly idempotencyTtlMs: number;\n /** Milliseconds a presence member lives without a heartbeat. Default 45,000. */\n readonly presenceTtlMs: number;\n /** Milliseconds between the heartbeats a client is asked to send (`ready.hb`). Default 30,000. */\n readonly heartbeatMs: number;\n /** Milliseconds between presence heartbeats (and reaping). Default 15,000. */\n readonly presenceHeartbeatMs: number;\n}\n\n/** The defaults of {@link HubLimits}. @since 1.0.0 */\nexport const DEFAULT_HUB_LIMITS: HubLimits = Object.freeze({\n maxMessageBytes: 65_536,\n maxChannelsPerConnection: 64,\n maxSubscribersPerChannel: 50_000,\n maxPresenceMembers: 1_000,\n maxStateBytes: 1_024,\n maxPendingRequests: 32,\n maxQueuedFrames: 256,\n maxBufferedBytes: 1_048_576,\n publishesPerSecond: 20,\n publishBurst: 40,\n channelPublishesPerSecond: 500,\n requestTimeoutMs: 10_000,\n history: 100,\n historyTtlMs: 300_000,\n idempotencyTtlMs: 60_000,\n presenceTtlMs: 45_000,\n presenceHeartbeatMs: 15_000,\n heartbeatMs: 30_000,\n});\n\n/** The most messages a channel may retain. */\nexport const MAX_HISTORY = 10_000;\n\n/**\n * Validates and fills in the limits.\n * @param given The limits the app set.\n * @returns The complete limits.\n * @throws RangeError for a limit that is not a positive integer (a history of 0 is allowed).\n */\nexport function resolveLimits(given: Partial<HubLimits> | undefined): HubLimits {\n const out: Record<string, number> = { ...DEFAULT_HUB_LIMITS };\n for (const [name, value] of Object.entries(given ?? {})) {\n if (!(name in DEFAULT_HUB_LIMITS))\n throw new TypeError(__DEV__ ? `unknown limit \"${name}\".` : \"REALTIME_LIMIT_2\");\n if (value === undefined) continue;\n const floor = name === \"history\" ? 0 : 1;\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < floor)\n throw new RangeError(\n __DEV__\n ? `limits.${name} must be ${floor === 0 ? \"a non-negative\" : \"a positive\"} integer.`\n : \"REALTIME_LIMIT_4\",\n );\n out[name] = value;\n }\n if ((out[\"history\"] as number) > MAX_HISTORY)\n throw new RangeError(\n __DEV__ ? `limits.history must be at most ${MAX_HISTORY}.` : \"REALTIME_LIMIT_1\",\n );\n if ((out[\"presenceTtlMs\"] as number) <= (out[\"presenceHeartbeatMs\"] as number))\n throw new RangeError(\n __DEV__\n ? \"limits.presenceTtlMs must be larger than limits.presenceHeartbeatMs.\"\n : \"REALTIME_LIMIT_3\",\n );\n return out as unknown as HubLimits;\n}\n\n/** A token bucket: `rate` tokens per second up to `burst`. */\nexport class TokenBucket {\n #tokens: number;\n #at: number;\n readonly #rate: number;\n readonly #burst: number;\n\n constructor(rate: number, burst: number, now: number) {\n this.#rate = rate;\n this.#burst = burst;\n this.#tokens = burst;\n this.#at = now;\n }\n\n /** Takes one token; false when the bucket is empty. */\n take(now: number): boolean {\n const elapsed = Math.max(0, now - this.#at);\n this.#at = now;\n this.#tokens = Math.min(this.#burst, this.#tokens + (elapsed * this.#rate) / 1000);\n if (this.#tokens < 1) return false;\n this.#tokens -= 1;\n return true;\n }\n}\n\n/** Counts events inside a sliding window; `hit` returns true once the count reaches the limit. */\nexport class WindowCounter {\n readonly #times: number[] = [];\n readonly #limit: number;\n readonly #windowMs: number;\n\n constructor(limit: number, windowMs: number) {\n this.#limit = limit;\n this.#windowMs = windowMs;\n }\n\n hit(now: number): boolean {\n this.#times.push(now);\n const cutoff = now - this.#windowMs;\n while (this.#times.length > 0 && (this.#times[0] as number) <= cutoff) this.#times.shift();\n return this.#times.length >= this.#limit;\n }\n}\n"],"mappings":"AA+CA,MAAaA,EAAgC,OAAO,OAAO,CACzD,gBAAiB,MACjB,yBAA0B,GAC1B,yBAA0B,IAC1B,mBAAoB,IACpB,cAAe,KACf,mBAAoB,GACpB,gBAAiB,IACjB,iBAAkB,QAClB,mBAAoB,GACpB,aAAc,GACd,0BAA2B,IAC3B,iBAAkB,IAClB,QAAS,IACT,aAAc,IACd,iBAAkB,IAClB,cAAe,KACf,oBAAqB,KACrB,YAAa,GACf,CAAC,EAGYC,EAAc,IAQ3B,SAAgBC,EAAc,EAAkD,CAC9E,IAAMC,EAA8B,CAAE,GAAGH,CAAmB,EAC5D,IAAK,GAAM,CAACI,EAAMC,KAAU,OAAO,QAAQC,GAAS,CAAC,CAAC,EAAG,CACvD,GAAI,EAAEF,KAAQJ,GACZ,MAAU,UAAiD,kBAAkB,EAC/E,GAAIK,IAAU,IAAA,GAAW,SACzB,IAAME,EAAQH,IAAS,UAAY,EAAI,EACvC,GAAI,OAAOC,GAAU,UAAY,CAAC,OAAO,UAAUA,CAAK,GAAKA,EAAQE,EACnE,MAAU,WAGJ,kBACN,EACF,EAAIH,GAAQC,CACd,CACA,GAAKF,EAAIK,QAAA,IACP,MAAU,WACqD,kBAC/D,EACF,GAAKL,EAAIM,eAAgCN,EAAIO,oBAC3C,MAAU,WAGJ,kBACN,EACF,OAAOP,CACT,CAGA,IAAaQ,EAAb,KAAyB,CACvB,GACA,GACA,GACA,GAEA,YAAY,EAAc,EAAe,EAAa,CACpD,KAAKC,GAAQC,EACb,KAAKC,GAASC,EACd,KAAKC,GAAUD,EACf,KAAKE,GAAMC,CACb,CAGA,KAAK,EAAsB,CACzB,IAAMC,EAAU,KAAK,IAAI,EAAGD,EAAM,KAAKD,EAAG,EAK1C,MAJA,MAAKA,GAAMC,EACX,KAAKF,GAAU,KAAK,IAAI,KAAKF,GAAQ,KAAKE,GAAWG,EAAU,KAAKP,GAAS,GAAI,EAC7E,KAAKI,GAAU,EAAU,IAC7B,OAAKA,GACE,GACT,CACF,EAGaI,EAAb,KAA2B,CACzB,GAA4B,CAAC,EAC7B,GACA,GAEA,YAAY,EAAe,EAAkB,CAC3C,KAAKC,GAASC,EACd,KAAKC,GAAYC,CACnB,CAEA,IAAI,EAAsB,CACxB,KAAKC,GAAO,KAAKP,CAAG,EACpB,IAAMQ,EAASR,EAAM,KAAKK,GAC1B,KAAO,KAAKE,GAAO,OAAS,GAAM,KAAKA,GAAO,IAAiBC,GAAQ,KAAKD,GAAO,MAAM,EACzF,OAAO,KAAKA,GAAO,QAAU,KAAKJ,EACpC,CACF,E"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/,t=/^\{\.\.\.([A-Za-z_][A-Za-z0-9_]*)\}$/,n=/^[A-Za-z0-9_.@/-]+$/;function r(r,i){if(typeof r!=`string`||r.length===0||r.length>128)throw TypeError(`
|
|
1
|
+
const e=/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/,t=/^\{\.\.\.([A-Za-z_][A-Za-z0-9_]*)\}$/,n=/^[A-Za-z0-9_.@/-]+$/;function r(r,i){if(typeof r!=`string`||r.length===0||r.length>128)throw TypeError(`REALTIME_PATTERN_1`);let a=r.split(`:`),o=/* @__PURE__ */ new Set,s=a.map((r,i)=>{let s=t.exec(r);if(s!==null){if(i!==a.length-1)throw TypeError(`REALTIME_PATTERN_3`);let e=s[1];if(o.has(e))throw TypeError(`REALTIME_PATTERN_2`);return o.add(e),{kind:`rest`,name:e}}let c=e.exec(r);if(c!==null){let e=c[1];if(o.has(e))throw TypeError(`REALTIME_PATTERN_2`);return o.add(e),{kind:`param`,name:e}}if(!n.test(r)||r.startsWith(`$`))throw TypeError(`REALTIME_PATTERN_4`);return{kind:`literal`,text:r}});if(i.history!==void 0&&(!Number.isInteger(i.history)||i.history<0))throw RangeError(`REALTIME_RULE`);return{pattern:r,parts:s,rule:i}}function i(e,t){let n=t.split(`:`);if(n.includes(``))return;let{parts:r}=e;if(r[r.length-1]?.kind===`rest`?n.length<r.length:n.length!==r.length)return;let i={};for(let e=0;e<r.length;e+=1){let t=r[e],a=n[e];if(t.kind===`rest`)return i[t.name]=n.slice(e).join(`:`),i;if(t.kind===`literal`){if(a!==t.text)return}else i[t.name]=a}return i}export{r as compileRule,i as matchRule};
|
|
2
2
|
//# sourceMappingURL=rules.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","names":["PARAM","REST","LITERAL","compileRule","pattern","segments","seen","parts","rest","segment","at","name","param","rule","matchRule","channel","compiled","params","part"],"sources":["../../../src/server/rules.ts"],"sourcesContent":["import type { Json } from \"../protocol\";\n\n/** Who a connection is. `user` identifies a person; other claims are the app's. @since 1.0.0 */\nexport interface Identity {\n readonly user?: string;\n readonly [claim: string]: unknown;\n}\n\n/** What a rule predicate is asked. @since 1.0.0 */\nexport interface RuleContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly op: \"subscribe\" | \"publish\" | \"presence\";\n}\n\n/** `true`, `false`, or a predicate that decides per connection. @since 1.0.0 */\nexport type Permission<Params extends Readonly<Record<string, string>> = Record<string, string>> =\n | boolean\n | ((context: RuleContext<Params>) => boolean | Promise<boolean>);\n\n/** What a request handler is given. @since 1.0.0 */\nexport interface RequestContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly event: string;\n readonly data: Json;\n /** Aborted when the connection closes, the request times out or the hub closes. */\n readonly signal: AbortSignal;\n}\n\n/** Answers a request. The returned value is the reply (JSON). @since 1.0.0 */\nexport type RequestHandler<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> = (context: RequestContext<Params>) => Json | undefined | Promise<Json | undefined>;\n\n/**\n * What a channel pattern allows. Every permission defaults to `false`: a channel nobody wrote a rule for is closed.\n * @since 1.0.0\n */\nexport interface ChannelRule<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly subscribe?: Permission<Params>;\n readonly publish?: Permission<Params>;\n readonly presence?: Permission<Params>;\n /** Messages kept for resume (default: the hub's `history` limit; 0 turns resume off for these channels). */\n readonly history?: number;\n /** Request handlers by event name. */\n readonly handlers?: Readonly<Record<string, RequestHandler<Params>>>;\n}\n\ntype Part =\n | { readonly kind: \"literal\"; readonly text: string }\n | { readonly kind: \"param\"; readonly name: string }\n | { readonly kind: \"rest\"; readonly name: string };\n\n/** A compiled rule. @internal */\nexport interface CompiledRule {\n readonly pattern: string;\n readonly parts: readonly Part[];\n readonly rule: ChannelRule;\n}\n\nconst PARAM = /^\\{([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst REST = /^\\{\\.\\.\\.([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst LITERAL = /^[A-Za-z0-9_.@/-]+$/;\n\n/**\n * Compiles a channel pattern: `:`-separated segments that are literals, `{name}` (one segment) or a final `{...name}` (one or\n * more segments).\n * @throws TypeError for an invalid pattern.\n */\nexport function compileRule(pattern: string, rule: ChannelRule): CompiledRule {\n if (typeof pattern !== \"string\" || pattern.length === 0 || pattern.length > 128)\n throw new TypeError(`the channel pattern \"${String(pattern)}\" must be 1..128 characters
|
|
1
|
+
{"version":3,"file":"rules.js","names":["PARAM","REST","LITERAL","compileRule","pattern","segments","seen","parts","rest","segment","at","name","param","rule","matchRule","channel","compiled","params","part"],"sources":["../../../src/server/rules.ts"],"sourcesContent":["import type { Json } from \"../protocol\";\n\n/** Who a connection is. `user` identifies a person; other claims are the app's. @since 1.0.0 */\nexport interface Identity {\n readonly user?: string;\n readonly [claim: string]: unknown;\n}\n\n/** What a rule predicate is asked. @since 1.0.0 */\nexport interface RuleContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly op: \"subscribe\" | \"publish\" | \"presence\";\n}\n\n/** `true`, `false`, or a predicate that decides per connection. @since 1.0.0 */\nexport type Permission<Params extends Readonly<Record<string, string>> = Record<string, string>> =\n | boolean\n | ((context: RuleContext<Params>) => boolean | Promise<boolean>);\n\n/** What a request handler is given. @since 1.0.0 */\nexport interface RequestContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly event: string;\n readonly data: Json;\n /** Aborted when the connection closes, the request times out or the hub closes. */\n readonly signal: AbortSignal;\n}\n\n/** Answers a request. The returned value is the reply (JSON). @since 1.0.0 */\nexport type RequestHandler<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> = (context: RequestContext<Params>) => Json | undefined | Promise<Json | undefined>;\n\n/**\n * What a channel pattern allows. Every permission defaults to `false`: a channel nobody wrote a rule for is closed.\n * @since 1.0.0\n */\nexport interface ChannelRule<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly subscribe?: Permission<Params>;\n readonly publish?: Permission<Params>;\n readonly presence?: Permission<Params>;\n /** Messages kept for resume (default: the hub's `history` limit; 0 turns resume off for these channels). */\n readonly history?: number;\n /** Request handlers by event name. */\n readonly handlers?: Readonly<Record<string, RequestHandler<Params>>>;\n}\n\ntype Part =\n | { readonly kind: \"literal\"; readonly text: string }\n | { readonly kind: \"param\"; readonly name: string }\n | { readonly kind: \"rest\"; readonly name: string };\n\n/** A compiled rule. @internal */\nexport interface CompiledRule {\n readonly pattern: string;\n readonly parts: readonly Part[];\n readonly rule: ChannelRule;\n}\n\nconst PARAM = /^\\{([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst REST = /^\\{\\.\\.\\.([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst LITERAL = /^[A-Za-z0-9_.@/-]+$/;\n\n/**\n * Compiles a channel pattern: `:`-separated segments that are literals, `{name}` (one segment) or a final `{...name}` (one or\n * more segments).\n * @throws TypeError for an invalid pattern.\n */\nexport function compileRule(pattern: string, rule: ChannelRule): CompiledRule {\n if (typeof pattern !== \"string\" || pattern.length === 0 || pattern.length > 128)\n throw new TypeError(\n __DEV__\n ? `the channel pattern \"${String(pattern)}\" must be 1..128 characters.`\n : \"REALTIME_PATTERN_1\",\n );\n const segments = pattern.split(\":\");\n const seen = new Set<string>();\n const parts = segments.map((segment, at): Part => {\n const rest = REST.exec(segment);\n if (rest !== null) {\n if (at !== segments.length - 1)\n throw new TypeError(\n __DEV__\n ? `the channel pattern \"${pattern}\": {...name} must be the last segment.`\n : \"REALTIME_PATTERN_3\",\n );\n const name = rest[1] as string;\n if (seen.has(name))\n throw new TypeError(\n __DEV__ ? `the channel pattern \"${pattern}\" repeats \"${name}\".` : \"REALTIME_PATTERN_2\",\n );\n seen.add(name);\n return { kind: \"rest\", name };\n }\n const param = PARAM.exec(segment);\n if (param !== null) {\n const name = param[1] as string;\n if (seen.has(name))\n throw new TypeError(\n __DEV__ ? `the channel pattern \"${pattern}\" repeats \"${name}\".` : \"REALTIME_PATTERN_2\",\n );\n seen.add(name);\n return { kind: \"param\", name };\n }\n if (!LITERAL.test(segment) || segment.startsWith(\"$\"))\n throw new TypeError(\n __DEV__\n ? `the channel pattern \"${pattern}\" has an invalid segment \"${segment}\".`\n : \"REALTIME_PATTERN_4\",\n );\n return { kind: \"literal\", text: segment };\n });\n if (rule.history !== undefined && (!Number.isInteger(rule.history) || rule.history < 0))\n throw new RangeError(\n __DEV__\n ? `the rule for \"${pattern}\": history must be a non-negative integer.`\n : \"REALTIME_RULE\",\n );\n return { pattern, parts, rule };\n}\n\n/** The params a name captures against a rule, or undefined when it does not match. */\nexport function matchRule(\n compiled: CompiledRule,\n channel: string,\n): Record<string, string> | undefined {\n const segments = channel.split(\":\");\n if (segments.includes(\"\")) return undefined;\n const { parts } = compiled;\n const last = parts[parts.length - 1];\n if (last?.kind === \"rest\" ? segments.length < parts.length : segments.length !== parts.length)\n return undefined;\n const params: Record<string, string> = {};\n for (let at = 0; at < parts.length; at += 1) {\n const part = parts[at] as Part;\n const segment = segments[at] as string;\n if (part.kind === \"rest\") {\n params[part.name] = segments.slice(at).join(\":\");\n return params;\n }\n if (part.kind === \"literal\") {\n if (segment !== part.text) return undefined;\n } else {\n params[part.name] = segment;\n }\n }\n return params;\n}\n"],"mappings":"AAqEA,MAAMA,EAAQ,iCACRC,EAAO,uCACPC,EAAU,sBAOhB,SAAgBC,EAAY,EAAiB,EAAiC,CAC5E,GAAI,OAAOC,GAAY,UAAYA,EAAQ,SAAW,GAAKA,EAAQ,OAAS,IAC1E,MAAU,UAGJ,oBACN,EACF,IAAMC,EAAWD,EAAQ,MAAM,GAAG,EAC5BE,kBAAO,IAAI,IACXC,EAAQF,EAAS,KAAK,EAAS,IAAa,CAChD,IAAMG,EAAOP,EAAK,KAAKQ,CAAO,EAC9B,GAAID,IAAS,KAAM,CACjB,GAAIE,IAAOL,EAAS,OAAS,EAC3B,MAAU,UAGJ,oBACN,EACF,IAAMM,EAAOH,EAAK,GAClB,GAAIF,EAAK,IAAIK,CAAI,EACf,MAAU,UAC0D,oBACpE,EAEF,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,OAAQ,MAAK,CAC9B,CACA,IAAMC,EAAQZ,EAAM,KAAKS,CAAO,EAChC,GAAIG,IAAU,KAAM,CAClB,IAAMD,EAAOC,EAAM,GACnB,GAAIN,EAAK,IAAIK,CAAI,EACf,MAAU,UAC0D,oBACpE,EAEF,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,QAAS,MAAK,CAC/B,CACA,GAAI,CAACT,EAAQ,KAAKO,CAAO,GAAKA,EAAQ,WAAW,GAAG,EAClD,MAAU,UAGJ,oBACN,EACF,MAAO,CAAE,KAAM,UAAW,KAAMA,CAAQ,CAC1C,CAAC,EACD,GAAII,EAAK,UAAY,IAAA,KAAc,CAAC,OAAO,UAAUA,EAAK,OAAO,GAAKA,EAAK,QAAU,GACnF,MAAU,WAGJ,eACN,EACF,MAAO,CAAE,UAAS,QAAO,MAAK,CAChC,CAGA,SAAgBC,EACd,EACA,EACoC,CACpC,IAAMT,EAAWU,EAAQ,MAAM,GAAG,EAClC,GAAIV,EAAS,SAAS,EAAE,EAAG,OAC3B,GAAM,CAAE,SAAUW,EAElB,GADaT,EAAMA,EAAM,OAAS,EAC1B,EAAE,OAAS,OAASF,EAAS,OAASE,EAAM,OAASF,EAAS,SAAWE,EAAM,OACrF,OACF,IAAMU,EAAiC,CAAC,EACxC,IAAK,IAAIP,EAAK,EAAGA,EAAKH,EAAM,OAAQ,GAAM,EAAG,CAC3C,IAAMW,EAAOX,EAAMG,GACbD,EAAUJ,EAASK,GACzB,GAAIQ,EAAK,OAAS,OAEhB,MADA,GAAOA,EAAK,MAAQb,EAAS,MAAMK,CAAE,CAAC,CAAC,KAAK,GAAG,EACxCO,EAET,GAAIC,EAAK,OAAS,UACZ,IAAAT,IAAYS,EAAK,KAAM,MAAO,KAElC,GAAOA,EAAK,MAAQT,CAExB,CACA,OAAOQ,CACT,C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadow-js/realtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Typed channels, presence and resumable delivery over WebSocket or server-sent events",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -77,18 +77,19 @@
|
|
|
77
77
|
"url": "https://github.com/shadowjs-dev/realtime/issues"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
|
-
"@shadow-js/core": "^1.0.
|
|
80
|
+
"@shadow-js/core": "^1.0.2"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@biomejs/biome": "2.5.15",
|
|
84
84
|
"@resvg/resvg-wasm": "2.6.2",
|
|
85
|
-
"@shadow-js/core": "^1.0.
|
|
86
|
-
"@shadow-js/dusk": "^1.0.
|
|
85
|
+
"@shadow-js/core": "^1.0.2",
|
|
86
|
+
"@shadow-js/dusk": "^1.0.2",
|
|
87
87
|
"@shadow-js/i18n": "*",
|
|
88
88
|
"@shadow-js/router": "*",
|
|
89
|
-
"@shadow-js/testing": "^1.0.
|
|
89
|
+
"@shadow-js/testing": "^1.0.2",
|
|
90
90
|
"@testing-library/dom": "10.4.2",
|
|
91
91
|
"@types/node": "^24.0.0",
|
|
92
|
+
"esbuild": "0.27.0",
|
|
92
93
|
"fast-check": "^4.10.2",
|
|
93
94
|
"happy-dom": "^21.0.3",
|
|
94
95
|
"satori": "0.43.0",
|