kempo 4.3.0 → 4.4.0
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/dist/kempo/realtime.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=1001,t=async()=>{try{const e=await fetch("/kempo/api/auth/session");if(!e.ok)return!0;const t=await e.json();return Boolean(t.user)}catch(e){return!0}};export class RealtimeClient{#e;#t;#s;#i;#n;#r;#o=null;#a="idle";#d=0;#c=null;#h=!1;#l=new Map;#u={status:new Set,ready:new Set,direct:new Set};#p=new Map;#f=1;userId=null;constructor({url:e,WebSocket:s=globalThis.WebSocket,checkSession:i=t,backoff:n={},random:r=Math.random}={}){this.#e=e,this.#t=s,this.#s=i,this.#i=n.base??1e3,this.#n=n.max??3e4,this.#r=r}get status(){return this.#a}onStatus=e=>(this.#u.status.add(e),()=>this.#u.status.delete(e));onReady=e=>(this.#u.ready.add(e),this.userId&&e(this.userId),()=>this.#u.ready.delete(e));onDirect=e=>(this.#u.direct.add(e),()=>this.#u.direct.delete(e));send=(e,t,{timeout:s=1e4}={})=>new Promise((i,n)=>{if(1!==this.#o?.readyState)return void n({code:503,msg:"Not connected"});const r=this.#f++,o=setTimeout(()=>{this.#p.delete(r),n({code:504,msg:"No reply from the server"})},s);this.#p.set(r,{resolve:i,reject:n,timer:o}),this.#m({type:"send",channel:e,data:t,ref:r})});subscribe=(e,t,{since:s,onGap:i,onError:n}={})=>{let
|
|
1
|
+
const e=1001,t=async()=>{try{const e=await fetch("/kempo/api/auth/session");if(!e.ok)return!0;const t=await e.json();return Boolean(t.user)}catch(e){return!0}};export class RealtimeClient{#e;#t;#s;#i;#n;#r;#o=null;#a="idle";#d=0;#c=null;#h=!1;#l=new Map;#u={status:new Set,ready:new Set,direct:new Set};#p=new Map;#f=1;userId=null;constructor({url:e,WebSocket:s=globalThis.WebSocket,checkSession:i=t,backoff:n={},random:r=Math.random}={}){this.#e=e,this.#t=s,this.#s=i,this.#i=n.base??1e3,this.#n=n.max??3e4,this.#r=r}get status(){return this.#a}onStatus=e=>(this.#u.status.add(e),()=>this.#u.status.delete(e));onReady=e=>(this.#u.ready.add(e),this.userId&&e(this.userId),()=>this.#u.ready.delete(e));onDirect=e=>(this.#u.direct.add(e),()=>this.#u.direct.delete(e));send=(e,t,{timeout:s=1e4}={})=>new Promise((i,n)=>{if(1!==this.#o?.readyState)return void n({code:503,msg:"Not connected"});const r=this.#f++,o=setTimeout(()=>{this.#p.delete(r),n({code:504,msg:"No reply from the server"})},s);this.#p.set(r,{resolve:i,reject:n,timer:o}),this.#m({type:"send",channel:e,data:t,ref:r})});subscribe=(e,t,{since:s,onGap:i,onError:n,onSubscribed:r}={})=>{let o=this.#l.get(e);return o||(o={handlers:new Set,gapHandlers:new Set,errorHandlers:new Set,subscribedHandlers:new Set,lastId:s??null,failed:!1,confirmed:!1},this.#l.set(e,o)),o.handlers.add(t),i&&o.gapHandlers.add(i),n&&o.errorHandlers.add(n),r&&o.subscribedHandlers.add(r),1===o.handlers.size?this.#b(e,o):r&&o.confirmed&&queueMicrotask(()=>this.#y(()=>r({channel:e}))),()=>{o.handlers.delete(t),i&&o.gapHandlers.delete(i),n&&o.errorHandlers.delete(n),r&&o.subscribedHandlers.delete(r),o.handlers.size||(this.#l.delete(e),this.#m({type:"unsubscribe",channel:e}))}};start=()=>{this.#o||this.#h||this.#g()};close=()=>{this.#h=!0,clearTimeout(this.#c),this.#c=null,this.#S({code:503,msg:"Client closed"}),this.#k("closed"),this.#o?.close(1e3),this.#o=null};#g=()=>{this.#k(this.#d>0?"reconnecting":"connecting");let e=!1,t=!1;const s=new this.#t(this.#e??`${"https:"===location.protocol?"wss:":"ws:"}//${location.host}/kempo/api/realtime`);this.#o=s;const i=i=>{t||(t=!0,this.#o===s&&(this.#o=null),this.#S({code:503,msg:"Connection lost"}),this.#w(i,e))};s.addEventListener("open",()=>{e=!0,this.#d=0,this.#k("open");for(const[e,t]of this.#l)t.confirmed=!1,this.#b(e,t)}),s.addEventListener("message",e=>this.#v(e.data)),s.addEventListener("close",e=>i(e.code)),s.addEventListener("error",()=>{e||i(1006)})};#w=async(t,s)=>{if(this.#h)return;if(4401===t)return this.#h=!0,void this.#k("unauthenticated");if(4429===t)return this.#h=!0,void this.#k("refused");if(!s&&t!==e){const e=await this.#s();if(this.#h)return;if(!1===e)return this.#h=!0,void this.#k("unauthenticated")}this.#k("reconnecting"),this.#d+=t===e?0:1;const i=Math.min(this.#n,this.#i*2**Math.max(0,this.#d-1)),n=t===e?Math.min(250,this.#i):i*(.5+.5*this.#r());this.#c=setTimeout(()=>{this.#c=null,this.#h||this.#g()},n)};#b=(e,t)=>{if(t.failed)return;const s={type:"subscribe",channel:e};null!==t.lastId&&(s.since=t.lastId),this.#m(s)};#m=e=>{1===this.#o?.readyState&&this.#o.send(JSON.stringify(e))};#v=e=>{let t;try{t=JSON.parse(e)}catch(e){return}if("ready"===t.type){this.userId=t.userId;for(const e of this.#u.ready)e(t.userId);return}if("direct"===t.type){for(const e of this.#u.direct)this.#y(()=>e(t.data));return}if(("ack"===t.type||"error"===t.type)&&void 0!==t.ref){const e=this.#p.get(t.ref);if(!e)return;return this.#p.delete(t.ref),clearTimeout(e.timer),void("ack"===t.type?e.resolve(t.data):e.reject({code:t.code,msg:t.msg}))}const s=this.#l.get(t.channel);if(s)if("subscribed"!==t.type)if("message"!==t.type)if("gap"!==t.type){if("error"===t.type){s.failed=!0;for(const e of s.errorHandlers)this.#y(()=>e({channel:t.channel,code:t.code,msg:t.msg}))}}else for(const e of s.gapHandlers)this.#y(()=>e({channel:t.channel}));else{void 0!==t.id&&(s.lastId=t.id);for(const e of s.handlers)this.#y(()=>e(t.data,{channel:t.channel,id:t.id}))}else{s.confirmed=!0;for(const e of s.subscribedHandlers)this.#y(()=>e({channel:t.channel}))}};#S=e=>{for(const[t,s]of this.#p)clearTimeout(s.timer),s.reject(e),this.#p.delete(t)};#y=e=>{try{e()}catch(e){console.error("[kempo realtime] a handler threw:",e)}};#k=e=>{if(this.#a!==e){this.#a=e;for(const t of this.#u.status)this.#y(()=>t(e))}}}export const connect=e=>{const t=new RealtimeClient(e);return t.start(),t};
|
|
2
2
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\realtime.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\kempo\\realtime.js","names":["GOING_AWAY","defaultCheckSession","async","response","fetch","ok","body","json","Boolean","user","error","RealtimeClient","url","WebSocketImpl","checkSession","base","max","random","socket","status","attempts","timer","stopped","subscriptions","Map","listeners","Set","ready","direct","pending","nextRef","userId","constructor","WebSocket","globalThis","backoff","Math","this","onStatus","listener","add","delete","onReady","onDirect","send","channel","data","timeout","Promise","resolve","reject","readyState","code","msg","ref","setTimeout","set","type","subscribe","handler","since","onGap","onError","subscription","get","handlers","gapHandlers","errorHandlers","lastId","failed","size","sendSubscribe","start","open","close","clearTimeout","rejectPending","setStatus","opened","finished","location","protocol","host","finish","closed","addEventListener","event","received","signedIn","exponential","min","delay","frame","JSON","stringify","raw","parse","safely","undefined","id","fn","console","connect","options","client"],"sources":["0"],"sourcesContent":["/*\n Browser client for kempo's realtime socket.\n\n import { connect } from '/kempo/realtime.js';\n\n const realtime = connect();\n const stop = realtime.subscribe('my-extension:status', (data, { id }) => { ... }, {\n onGap: () => refetchEverything()\n });\n\n // Send to the channel's handler on the server, and wait for its reply\n const reply = await realtime.send('my-extension:status', { action: 'ping' });\n\n A browser does not reconnect a WebSocket that closes, so this does: with backoff, resubscribing to\n every channel and asking for what it missed since the last message it saw. It stops for good when the\n session has ended, since retrying could never succeed.\n*/\n\nconst DEFAULT_PATH = '/kempo/api/realtime';\nconst SESSION_ENDED = 4401;\nconst TOO_MANY_CONNECTIONS = 4429;\nconst GOING_AWAY = 1001;\n\nconst defaultUrl = () => `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}${DEFAULT_PATH}`;\n\n/*\n Distinguishes \"signed out\" from \"the network is down\". A failed handshake looks identical either way\n to a browser (a bare close with no status), so this asks the session endpoint. Only a definite answer\n that there is no session stops the retrying; anything unclear keeps trying.\n*/\nconst defaultCheckSession = async () => {\n try {\n const response = await fetch('/kempo/api/auth/session');\n if(!response.ok) return true;\n const body = await response.json();\n return Boolean(body.user);\n } catch(error) {\n return true;\n }\n};\n\nexport class RealtimeClient {\n #url;\n #WebSocketImpl;\n #checkSession;\n #base;\n #max;\n #random;\n #socket = null;\n #status = 'idle';\n #attempts = 0;\n #timer = null;\n #stopped = false;\n #subscriptions = new Map();\n #listeners = { status: new Set(), ready: new Set(), direct: new Set() };\n #pending = new Map();\n #nextRef = 1;\n\n userId = null;\n\n constructor({\n url,\n WebSocket: WebSocketImpl = globalThis.WebSocket,\n checkSession = defaultCheckSession,\n backoff = {},\n random = Math.random\n } = {}){\n this.#url = url;\n this.#WebSocketImpl = WebSocketImpl;\n this.#checkSession = checkSession;\n this.#base = backoff.base ?? 1000;\n this.#max = backoff.max ?? 30000;\n this.#random = random;\n }\n\n /*\n State\n */\n\n get status(){\n return this.#status;\n }\n\n onStatus = (listener) => {\n this.#listeners.status.add(listener);\n return () => this.#listeners.status.delete(listener);\n };\n\n onReady = (listener) => {\n this.#listeners.ready.add(listener);\n if(this.userId) listener(this.userId);\n return () => this.#listeners.ready.delete(listener);\n };\n\n // Messages the server sent to this connection alone, rather than to a channel\n onDirect = (listener) => {\n this.#listeners.direct.add(listener);\n return () => this.#listeners.direct.delete(listener);\n };\n\n /*\n Sending\n\n A message goes to the channel's handler on the server, and the promise settles with what the handler\n returned, or rejects with { code, msg }. It is not queued while disconnected: a message a game or an\n editor sends is stale by the time the connection comes back, so the caller is told it did not go.\n */\n\n send = (channel, data, { timeout = 10000 } = {}) => new Promise((resolve, reject) => {\n if(this.#socket?.readyState !== 1){\n reject({ code: 503, msg: 'Not connected' });\n return;\n }\n\n const ref = this.#nextRef++;\n const timer = setTimeout(() => {\n this.#pending.delete(ref);\n reject({ code: 504, msg: 'No reply from the server' });\n }, timeout);\n\n this.#pending.set(ref, { resolve, reject, timer });\n this.#send({ type: 'send', channel, data, ref });\n });\n\n /*\n Subscriptions\n\n Several handlers can share one channel; the server is only told when the first arrives and when the\n last leaves. `since` lets a caller that remembers the last id it processed (across a page load, say)\n resume from there rather than from now.\n */\n\n subscribe = (channel, handler, { since, onGap, onError } = {}) => {\n let subscription = this.#subscriptions.get(channel);\n\n if(!subscription){\n subscription = { handlers: new Set(), gapHandlers: new Set(), errorHandlers: new Set(), lastId: since ?? null, failed: false };\n this.#subscriptions.set(channel, subscription);\n }\n\n subscription.handlers.add(handler);\n if(onGap) subscription.gapHandlers.add(onGap);\n if(onError) subscription.errorHandlers.add(onError);\n\n // A repeat handler on a channel the server already confirmed needs no frame; a first one does\n if(subscription.handlers.size === 1) this.#sendSubscribe(channel, subscription);\n\n return () => {\n subscription.handlers.delete(handler);\n if(onGap) subscription.gapHandlers.delete(onGap);\n if(onError) subscription.errorHandlers.delete(onError);\n\n if(!subscription.handlers.size){\n this.#subscriptions.delete(channel);\n this.#send({ type: 'unsubscribe', channel });\n }\n };\n };\n\n /*\n Lifecycle\n */\n\n start = () => {\n if(this.#socket || this.#stopped) return;\n this.#open();\n };\n\n close = () => {\n this.#stopped = true;\n clearTimeout(this.#timer);\n this.#timer = null;\n this.#rejectPending({ code: 503, msg: 'Client closed' });\n this.#setStatus('closed');\n this.#socket?.close(1000);\n this.#socket = null;\n };\n\n #open = () => {\n this.#setStatus(this.#attempts > 0 ? 'reconnecting' : 'connecting');\n\n let opened = false;\n let finished = false;\n const socket = new this.#WebSocketImpl(this.#url ?? defaultUrl());\n this.#socket = socket;\n\n // One attempt ends once, however many of error and close report it\n const finish = (code) => {\n if(finished) return;\n finished = true;\n if(this.#socket === socket) this.#socket = null;\n this.#rejectPending({ code: 503, msg: 'Connection lost' });\n this.#closed(code, opened);\n };\n\n socket.addEventListener('open', () => {\n opened = true;\n this.#attempts = 0;\n this.#setStatus('open');\n for(const [channel, subscription] of this.#subscriptions){\n this.#sendSubscribe(channel, subscription);\n }\n });\n\n socket.addEventListener('message', event => this.#received(event.data));\n\n socket.addEventListener('close', event => finish(event.code));\n\n /*\n A browser follows a failed connection with a close event; Node's WebSocket sends only the error. A\n client that waited for close would then never retry, so a failure before the socket opened ends the\n attempt on its own. After it opened, an error is always followed by a close carrying the real code.\n */\n socket.addEventListener('error', () => {\n if(!opened) finish(1006);\n });\n };\n\n #closed = async (code, opened) => {\n if(this.#stopped) return;\n\n if(code === SESSION_ENDED){\n this.#stopped = true;\n this.#setStatus('unauthenticated');\n return;\n }\n\n // The user is at their connection limit; reconnecting would only be refused again\n if(code === TOO_MANY_CONNECTIONS){\n this.#stopped = true;\n this.#setStatus('refused');\n return;\n }\n\n /*\n A handshake that never completed is either a network problem or a refused session, and the browser\n cannot say which, so ask. A restart (1001) is neither, so it skips the check and comes straight back.\n */\n if(!opened && code !== GOING_AWAY){\n const signedIn = await this.#checkSession();\n if(this.#stopped) return;\n if(signedIn === false){\n this.#stopped = true;\n this.#setStatus('unauthenticated');\n return;\n }\n }\n\n this.#setStatus('reconnecting');\n this.#attempts += code === GOING_AWAY ? 0 : 1;\n\n const exponential = Math.min(this.#max, this.#base * 2 ** Math.max(0, this.#attempts - 1));\n // Jitter, so a restart does not bring every client back in the same instant\n const delay = code === GOING_AWAY ? Math.min(250, this.#base) : exponential * (0.5 + this.#random() * 0.5);\n\n this.#timer = setTimeout(() => {\n this.#timer = null;\n if(!this.#stopped) this.#open();\n }, delay);\n };\n\n /*\n Frames\n */\n\n #sendSubscribe = (channel, subscription) => {\n if(subscription.failed) return;\n const frame = { type: 'subscribe', channel };\n if(subscription.lastId !== null) frame.since = subscription.lastId;\n this.#send(frame);\n };\n\n #send = (frame) => {\n if(this.#socket?.readyState === 1) this.#socket.send(JSON.stringify(frame));\n };\n\n #received = (raw) => {\n let frame;\n try {\n frame = JSON.parse(raw);\n } catch(error) {\n return;\n }\n\n if(frame.type === 'ready'){\n this.userId = frame.userId;\n for(const listener of this.#listeners.ready) listener(frame.userId);\n return;\n }\n\n if(frame.type === 'direct'){\n for(const listener of this.#listeners.direct) this.#safely(() => listener(frame.data));\n return;\n }\n\n // A reply to a send(), matched by its ref before anything is routed by channel\n if((frame.type === 'ack' || frame.type === 'error') && frame.ref !== undefined){\n const pending = this.#pending.get(frame.ref);\n if(!pending) return;\n this.#pending.delete(frame.ref);\n clearTimeout(pending.timer);\n if(frame.type === 'ack') pending.resolve(frame.data);\n else pending.reject({ code: frame.code, msg: frame.msg });\n return;\n }\n\n const subscription = this.#subscriptions.get(frame.channel);\n if(!subscription) return;\n\n if(frame.type === 'message'){\n if(frame.id !== undefined) subscription.lastId = frame.id;\n for(const handler of subscription.handlers) this.#safely(() => handler(frame.data, { channel: frame.channel, id: frame.id }));\n return;\n }\n\n if(frame.type === 'gap'){\n for(const handler of subscription.gapHandlers) this.#safely(() => handler({ channel: frame.channel }));\n return;\n }\n\n if(frame.type === 'error'){\n // A refused subscription would be refused again on every reconnect, so it is not retried\n subscription.failed = true;\n for(const handler of subscription.errorHandlers) this.#safely(() => handler({ channel: frame.channel, code: frame.code, msg: frame.msg }));\n }\n };\n\n #rejectPending = (error) => {\n for(const [ref, pending] of this.#pending){\n clearTimeout(pending.timer);\n pending.reject(error);\n this.#pending.delete(ref);\n }\n };\n\n // One handler throwing must not stop the others, or the frames behind it\n #safely = (fn) => {\n try {\n fn();\n } catch(error) {\n console.error('[kempo realtime] a handler threw:', error);\n }\n };\n\n #setStatus = (status) => {\n if(this.#status === status) return;\n this.#status = status;\n for(const listener of this.#listeners.status) this.#safely(() => listener(status));\n };\n}\n\nexport const connect = (options) => {\n const client = new RealtimeClient(options);\n client.start();\n return client;\n};\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"src\\kempo\\realtime.js","names":["GOING_AWAY","defaultCheckSession","async","response","fetch","ok","body","json","Boolean","user","error","RealtimeClient","url","WebSocketImpl","checkSession","base","max","random","socket","status","attempts","timer","stopped","subscriptions","Map","listeners","Set","ready","direct","pending","nextRef","userId","constructor","WebSocket","globalThis","backoff","Math","this","onStatus","listener","add","delete","onReady","onDirect","send","channel","data","timeout","Promise","resolve","reject","readyState","code","msg","ref","setTimeout","set","type","subscribe","handler","since","onGap","onError","onSubscribed","subscription","get","handlers","gapHandlers","errorHandlers","subscribedHandlers","lastId","failed","confirmed","size","sendSubscribe","queueMicrotask","safely","start","open","close","clearTimeout","rejectPending","setStatus","opened","finished","location","protocol","host","finish","closed","addEventListener","event","received","signedIn","exponential","min","delay","frame","JSON","stringify","raw","parse","undefined","id","fn","console","connect","options","client"],"sources":["0"],"sourcesContent":["/*\n Browser client for kempo's realtime socket.\n\n import { connect } from '/kempo/realtime.js';\n\n const realtime = connect();\n const stop = realtime.subscribe('my-extension:status', (data, { id }) => { ... }, {\n onGap: () => refetchEverything()\n });\n\n // Send to the channel's handler on the server, and wait for its reply\n const reply = await realtime.send('my-extension:status', { action: 'ping' });\n\n A browser does not reconnect a WebSocket that closes, so this does: with backoff, resubscribing to\n every channel and asking for what it missed since the last message it saw. It stops for good when the\n session has ended, since retrying could never succeed.\n*/\n\nconst DEFAULT_PATH = '/kempo/api/realtime';\nconst SESSION_ENDED = 4401;\nconst TOO_MANY_CONNECTIONS = 4429;\nconst GOING_AWAY = 1001;\n\nconst defaultUrl = () => `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}${DEFAULT_PATH}`;\n\n/*\n Distinguishes \"signed out\" from \"the network is down\". A failed handshake looks identical either way\n to a browser (a bare close with no status), so this asks the session endpoint. Only a definite answer\n that there is no session stops the retrying; anything unclear keeps trying.\n*/\nconst defaultCheckSession = async () => {\n try {\n const response = await fetch('/kempo/api/auth/session');\n if(!response.ok) return true;\n const body = await response.json();\n return Boolean(body.user);\n } catch(error) {\n return true;\n }\n};\n\nexport class RealtimeClient {\n #url;\n #WebSocketImpl;\n #checkSession;\n #base;\n #max;\n #random;\n #socket = null;\n #status = 'idle';\n #attempts = 0;\n #timer = null;\n #stopped = false;\n #subscriptions = new Map();\n #listeners = { status: new Set(), ready: new Set(), direct: new Set() };\n #pending = new Map();\n #nextRef = 1;\n\n userId = null;\n\n constructor({\n url,\n WebSocket: WebSocketImpl = globalThis.WebSocket,\n checkSession = defaultCheckSession,\n backoff = {},\n random = Math.random\n } = {}){\n this.#url = url;\n this.#WebSocketImpl = WebSocketImpl;\n this.#checkSession = checkSession;\n this.#base = backoff.base ?? 1000;\n this.#max = backoff.max ?? 30000;\n this.#random = random;\n }\n\n /*\n State\n */\n\n get status(){\n return this.#status;\n }\n\n onStatus = (listener) => {\n this.#listeners.status.add(listener);\n return () => this.#listeners.status.delete(listener);\n };\n\n onReady = (listener) => {\n this.#listeners.ready.add(listener);\n if(this.userId) listener(this.userId);\n return () => this.#listeners.ready.delete(listener);\n };\n\n // Messages the server sent to this connection alone, rather than to a channel\n onDirect = (listener) => {\n this.#listeners.direct.add(listener);\n return () => this.#listeners.direct.delete(listener);\n };\n\n /*\n Sending\n\n A message goes to the channel's handler on the server, and the promise settles with what the handler\n returned, or rejects with { code, msg }. It is not queued while disconnected: a message a game or an\n editor sends is stale by the time the connection comes back, so the caller is told it did not go.\n */\n\n send = (channel, data, { timeout = 10000 } = {}) => new Promise((resolve, reject) => {\n if(this.#socket?.readyState !== 1){\n reject({ code: 503, msg: 'Not connected' });\n return;\n }\n\n const ref = this.#nextRef++;\n const timer = setTimeout(() => {\n this.#pending.delete(ref);\n reject({ code: 504, msg: 'No reply from the server' });\n }, timeout);\n\n this.#pending.set(ref, { resolve, reject, timer });\n this.#send({ type: 'send', channel, data, ref });\n });\n\n /*\n Subscriptions\n\n Several handlers can share one channel; the server is only told when the first arrives and when the\n last leaves. `since` lets a caller that remembers the last id it processed (across a page load, say)\n resume from there rather than from now.\n */\n\n subscribe = (channel, handler, { since, onGap, onError, onSubscribed } = {}) => {\n let subscription = this.#subscriptions.get(channel);\n\n if(!subscription){\n subscription = { handlers: new Set(), gapHandlers: new Set(), errorHandlers: new Set(), subscribedHandlers: new Set(), lastId: since ?? null, failed: false, confirmed: false };\n this.#subscriptions.set(channel, subscription);\n }\n\n subscription.handlers.add(handler);\n if(onGap) subscription.gapHandlers.add(onGap);\n if(onError) subscription.errorHandlers.add(onError);\n if(onSubscribed) subscription.subscribedHandlers.add(onSubscribed);\n\n // A repeat handler on a channel the server already confirmed needs no frame; a first one does\n if(subscription.handlers.size === 1) this.#sendSubscribe(channel, subscription);\n // The server will not confirm again, so a late arrival is told straight away what it is waiting for\n else if(onSubscribed && subscription.confirmed) queueMicrotask(() => this.#safely(() => onSubscribed({ channel })));\n\n return () => {\n subscription.handlers.delete(handler);\n if(onGap) subscription.gapHandlers.delete(onGap);\n if(onError) subscription.errorHandlers.delete(onError);\n if(onSubscribed) subscription.subscribedHandlers.delete(onSubscribed);\n\n if(!subscription.handlers.size){\n this.#subscriptions.delete(channel);\n this.#send({ type: 'unsubscribe', channel });\n }\n };\n };\n\n /*\n Lifecycle\n */\n\n start = () => {\n if(this.#socket || this.#stopped) return;\n this.#open();\n };\n\n close = () => {\n this.#stopped = true;\n clearTimeout(this.#timer);\n this.#timer = null;\n this.#rejectPending({ code: 503, msg: 'Client closed' });\n this.#setStatus('closed');\n this.#socket?.close(1000);\n this.#socket = null;\n };\n\n #open = () => {\n this.#setStatus(this.#attempts > 0 ? 'reconnecting' : 'connecting');\n\n let opened = false;\n let finished = false;\n const socket = new this.#WebSocketImpl(this.#url ?? defaultUrl());\n this.#socket = socket;\n\n // One attempt ends once, however many of error and close report it\n const finish = (code) => {\n if(finished) return;\n finished = true;\n if(this.#socket === socket) this.#socket = null;\n this.#rejectPending({ code: 503, msg: 'Connection lost' });\n this.#closed(code, opened);\n };\n\n socket.addEventListener('open', () => {\n opened = true;\n this.#attempts = 0;\n this.#setStatus('open');\n for(const [channel, subscription] of this.#subscriptions){\n subscription.confirmed = false;\n this.#sendSubscribe(channel, subscription);\n }\n });\n\n socket.addEventListener('message', event => this.#received(event.data));\n\n socket.addEventListener('close', event => finish(event.code));\n\n /*\n A browser follows a failed connection with a close event; Node's WebSocket sends only the error. A\n client that waited for close would then never retry, so a failure before the socket opened ends the\n attempt on its own. After it opened, an error is always followed by a close carrying the real code.\n */\n socket.addEventListener('error', () => {\n if(!opened) finish(1006);\n });\n };\n\n #closed = async (code, opened) => {\n if(this.#stopped) return;\n\n if(code === SESSION_ENDED){\n this.#stopped = true;\n this.#setStatus('unauthenticated');\n return;\n }\n\n // The user is at their connection limit; reconnecting would only be refused again\n if(code === TOO_MANY_CONNECTIONS){\n this.#stopped = true;\n this.#setStatus('refused');\n return;\n }\n\n /*\n A handshake that never completed is either a network problem or a refused session, and the browser\n cannot say which, so ask. A restart (1001) is neither, so it skips the check and comes straight back.\n */\n if(!opened && code !== GOING_AWAY){\n const signedIn = await this.#checkSession();\n if(this.#stopped) return;\n if(signedIn === false){\n this.#stopped = true;\n this.#setStatus('unauthenticated');\n return;\n }\n }\n\n this.#setStatus('reconnecting');\n this.#attempts += code === GOING_AWAY ? 0 : 1;\n\n const exponential = Math.min(this.#max, this.#base * 2 ** Math.max(0, this.#attempts - 1));\n // Jitter, so a restart does not bring every client back in the same instant\n const delay = code === GOING_AWAY ? Math.min(250, this.#base) : exponential * (0.5 + this.#random() * 0.5);\n\n this.#timer = setTimeout(() => {\n this.#timer = null;\n if(!this.#stopped) this.#open();\n }, delay);\n };\n\n /*\n Frames\n */\n\n #sendSubscribe = (channel, subscription) => {\n if(subscription.failed) return;\n const frame = { type: 'subscribe', channel };\n if(subscription.lastId !== null) frame.since = subscription.lastId;\n this.#send(frame);\n };\n\n #send = (frame) => {\n if(this.#socket?.readyState === 1) this.#socket.send(JSON.stringify(frame));\n };\n\n #received = (raw) => {\n let frame;\n try {\n frame = JSON.parse(raw);\n } catch(error) {\n return;\n }\n\n if(frame.type === 'ready'){\n this.userId = frame.userId;\n for(const listener of this.#listeners.ready) listener(frame.userId);\n return;\n }\n\n if(frame.type === 'direct'){\n for(const listener of this.#listeners.direct) this.#safely(() => listener(frame.data));\n return;\n }\n\n // A reply to a send(), matched by its ref before anything is routed by channel\n if((frame.type === 'ack' || frame.type === 'error') && frame.ref !== undefined){\n const pending = this.#pending.get(frame.ref);\n if(!pending) return;\n this.#pending.delete(frame.ref);\n clearTimeout(pending.timer);\n if(frame.type === 'ack') pending.resolve(frame.data);\n else pending.reject({ code: frame.code, msg: frame.msg });\n return;\n }\n\n const subscription = this.#subscriptions.get(frame.channel);\n if(!subscription) return;\n\n // The server has granted the subscription, on the first connection and again after every reconnect\n if(frame.type === 'subscribed'){\n subscription.confirmed = true;\n for(const handler of subscription.subscribedHandlers) this.#safely(() => handler({ channel: frame.channel }));\n return;\n }\n\n if(frame.type === 'message'){\n if(frame.id !== undefined) subscription.lastId = frame.id;\n for(const handler of subscription.handlers) this.#safely(() => handler(frame.data, { channel: frame.channel, id: frame.id }));\n return;\n }\n\n if(frame.type === 'gap'){\n for(const handler of subscription.gapHandlers) this.#safely(() => handler({ channel: frame.channel }));\n return;\n }\n\n if(frame.type === 'error'){\n // A refused subscription would be refused again on every reconnect, so it is not retried\n subscription.failed = true;\n for(const handler of subscription.errorHandlers) this.#safely(() => handler({ channel: frame.channel, code: frame.code, msg: frame.msg }));\n }\n };\n\n #rejectPending = (error) => {\n for(const [ref, pending] of this.#pending){\n clearTimeout(pending.timer);\n pending.reject(error);\n this.#pending.delete(ref);\n }\n };\n\n // One handler throwing must not stop the others, or the frames behind it\n #safely = (fn) => {\n try {\n fn();\n } catch(error) {\n console.error('[kempo realtime] a handler threw:', error);\n }\n };\n\n #setStatus = (status) => {\n if(this.#status === status) return;\n this.#status = status;\n for(const listener of this.#listeners.status) this.#safely(() => listener(status));\n };\n}\n\nexport const connect = (options) => {\n const client = new RealtimeClient(options);\n client.start();\n return client;\n};\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -230,6 +230,25 @@ export default class Hub {
|
|
|
230
230
|
return [null, { unsubscribed: true }];
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
/*
|
|
234
|
+
The channel has been removed: every subscriber this process holds is unsubscribed and told, with the
|
|
235
|
+
same error frame a refused subscription gets, so the browser client stops treating it as live.
|
|
236
|
+
*/
|
|
237
|
+
dropChannel = ({ channel, reason = 'channel_removed' } = {}) => {
|
|
238
|
+
let dropped = 0;
|
|
239
|
+
for(const id of [...(this.#channels.get(channel) || [])]){
|
|
240
|
+
const subscriber = this.#subscribers.get(id);
|
|
241
|
+
this.unsubscribe({ id, channel, reason });
|
|
242
|
+
try {
|
|
243
|
+
subscriber?.deliver({ type: 'error', channel, code: 410, msg: 'This channel has closed' });
|
|
244
|
+
} catch(error) {
|
|
245
|
+
this.#log.error(`[realtime] could not tell ${id} that ${channel} closed: ${error.message}`);
|
|
246
|
+
}
|
|
247
|
+
dropped++;
|
|
248
|
+
}
|
|
249
|
+
return [null, { dropped }];
|
|
250
|
+
};
|
|
251
|
+
|
|
233
252
|
/*
|
|
234
253
|
Messages from clients
|
|
235
254
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
sockets belong to the process that accepted them.
|
|
7
7
|
*/
|
|
8
8
|
export { registerChannel } from './channels.js';
|
|
9
|
+
export { default as unregisterChannel } from './unregisterChannel.js';
|
|
9
10
|
export { default as publish } from './publish.js';
|
|
10
11
|
export { default as sendToConnection } from './sendToConnection.js';
|
|
11
12
|
export { default as closeConnection } from './closeConnection.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import getHub from './getHub.js';
|
|
2
|
+
import { unregisterChannel } from './channels.js';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Removes a channel that code registered, and lets go of everyone subscribed to it on this process.
|
|
6
|
+
|
|
7
|
+
For a channel that exists only while something else does, such as one per running game. Register it
|
|
8
|
+
before anyone can be told its name, and remove it when the thing ends. A channel an extension declares
|
|
9
|
+
in kempo-config.json is not in the registry and is unaffected.
|
|
10
|
+
*/
|
|
11
|
+
export default ({ channel } = {}) => {
|
|
12
|
+
if(typeof channel !== 'string' || !channel){
|
|
13
|
+
return [{ code: 400, msg: 'A channel name is required' }, null];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const removed = unregisterChannel(channel);
|
|
17
|
+
const [, { dropped }] = getHub().dropChannel({ channel });
|
|
18
|
+
return [null, { removed, dropped }];
|
|
19
|
+
};
|