diff --git a/.github/workflows/release/versions.json b/.github/workflows/release/versions.json index 51884f614..dc23f3b23 100644 --- a/.github/workflows/release/versions.json +++ b/.github/workflows/release/versions.json @@ -20,7 +20,7 @@ "clearedSuffix": false } ], - "src/core/components/config.js": [ + "src/core/components/configuration.ts": [ { "pattern": "^\\s{2,}return '(v?(\\.?\\d+){2,}([a-zA-Z0-9-]+(\\.?\\d+)?)?)';$", "clearedPrefix": true, diff --git a/dist/web/pubnub.js b/dist/web/pubnub.js index 4fa0dd604..92b49723d 100644 --- a/dist/web/pubnub.js +++ b/dist/web/pubnub.js @@ -3672,12 +3672,13 @@ var uuidGenerator$1 = /*@__PURE__*/getDefaultExportFromCjs(uuidExports); var uuidGenerator = { - createUUID() { - if (uuidGenerator$1.uuid) { - return uuidGenerator$1.uuid(); - } - return uuidGenerator$1(); - }, + createUUID() { + if (uuidGenerator$1.uuid) { + return uuidGenerator$1.uuid(); + } + // @ts-expect-error Depending on module type it may be callable. + return uuidGenerator$1(); + }, }; /** @@ -6450,7 +6451,6 @@ channels, groups, })); - // TODO: Find out actual `status` type. /* eslint-disable @typescript-eslint/no-explicit-any */ const emitStatus$1 = createEffect('EMIT_STATUS', (status) => status); const wait = createManagedEffect('WAIT', () => ({})); @@ -6685,7 +6685,6 @@ return { delay: configuration.delay, maximumRetry: configuration.maximumRetry, - // TODO: Find out actual `error` type. /* eslint-disable @typescript-eslint/no-explicit-any */ shouldRetry(error, attempt) { var _a; @@ -6699,7 +6698,6 @@ const delay = (_a = reason.retryAfter) !== null && _a !== void 0 ? _a : this.delay; return (delay + Math.random()) * 1000; }, - // TODO: Find out actual `error` type. /* eslint-disable @typescript-eslint/no-explicit-any */ getGiveupReason(error, attempt) { var _a; diff --git a/dist/web/pubnub.worker.js b/dist/web/pubnub.worker.js index 6470d2b27..31a3ef3ac 100644 --- a/dist/web/pubnub.worker.js +++ b/dist/web/pubnub.worker.js @@ -90,12 +90,13 @@ var uuidGenerator$1 = /*@__PURE__*/getDefaultExportFromCjs(uuidExports); var uuidGenerator = { - createUUID() { - if (uuidGenerator$1.uuid) { - return uuidGenerator$1.uuid(); - } - return uuidGenerator$1(); - }, + createUUID() { + if (uuidGenerator$1.uuid) { + return uuidGenerator$1.uuid(); + } + // @ts-expect-error Depending on module type it may be callable. + return uuidGenerator$1(); + }, }; /// @@ -208,6 +209,11 @@ markRequestCompleted(clients, requestOrId.identifier); }); }; + /** + * Handle client request to leave request. + * + * @param event - Leave event details. + */ const handleSendLeaveRequestEvent = (event) => { const data = event.data; const request = leaveTransportRequestFromEvent(data); @@ -222,7 +228,10 @@ result.url = `${data.request.origin}${data.request.path}`; result.clientIdentifier = data.clientIdentifier; result.identifier = data.request.identifier; - publishClientEvent(event.source.id, result); + publishClientEvent(event.source.id, result).then((sent) => { + if (sent) + invalidateClient(client.subscriptionKey, client.clientIdentifier, client.userId); + }); return; } sendRequest(request, () => [client], (clients, response) => { @@ -504,10 +513,11 @@ * @param event - Service worker event object. */ const publishClientEvent = (identifier, event) => { - self.clients.get(identifier).then((client) => { + return self.clients.get(identifier).then((client) => { if (!client) - return; + return false; client.postMessage(event); + return true; }); }; /** @@ -559,7 +569,10 @@ const { request: clientRequest } = client.subscription; const decidedRequest = clientRequest !== null && clientRequest !== void 0 ? clientRequest : request; if (client.logVerbosity && serviceWorkerClientId && decidedRequest) { - publishClientEvent(serviceWorkerClientId, Object.assign(Object.assign({}, event), { clientIdentifier: client.clientIdentifier, url: `${decidedRequest.origin}${decidedRequest.path}`, query: decidedRequest.queryParameters })); + publishClientEvent(serviceWorkerClientId, Object.assign(Object.assign({}, event), { clientIdentifier: client.clientIdentifier, url: `${decidedRequest.origin}${decidedRequest.path}`, query: decidedRequest.queryParameters })).then((sent) => { + if (sent) + invalidateClient(client.subscriptionKey, client.clientIdentifier, client.userId); + }); } }); }; @@ -590,7 +603,10 @@ const { request: clientRequest } = client.subscription; const decidedRequest = clientRequest !== null && clientRequest !== void 0 ? clientRequest : request; if (serviceWorkerClientId && decidedRequest) { - publishClientEvent(serviceWorkerClientId, Object.assign(Object.assign({}, result), { clientIdentifier: client.clientIdentifier, identifier: decidedRequest.identifier, url: `${decidedRequest.origin}${decidedRequest.path}` })); + publishClientEvent(serviceWorkerClientId, Object.assign(Object.assign({}, result), { clientIdentifier: client.clientIdentifier, identifier: decidedRequest.identifier, url: `${decidedRequest.origin}${decidedRequest.path}` })).then((sent) => { + if (sent) + invalidateClient(client.subscriptionKey, client.clientIdentifier, client.userId); + }); } }); }; @@ -696,7 +712,6 @@ userId: query.uuid, authKey: ((_c = query.auth) !== null && _c !== void 0 ? _c : ''), logVerbosity: information.logVerbosity, - lastAvailabilityCheck: new Date().getTime(), subscription: { path: !isPresenceLeave ? information.request.path : '', channelGroupQuery: !isPresenceLeave ? channelGroupQuery : '', @@ -728,7 +743,6 @@ client.subscription.filterExpression = ((_o = query['filter-expr']) !== null && _o !== void 0 ? _o : ''); client.subscription.previousTimetoken = client.subscription.timetoken; client.subscription.timetoken = ((_p = query.tt) !== null && _p !== void 0 ? _p : '0'); - client.lastAvailabilityCheck = new Date().getTime(); client.subscription.request = information.request; client.authKey = ((_q = query.auth) !== null && _q !== void 0 ? _q : ''); client.userId = query.uuid; @@ -759,6 +773,40 @@ } } }; + /** + * Clean up resources used by registered PubNub client instance. + * + * @param subscriptionKey - Subscription key which has been used by the + * invalidated instance. + * @param clientId - Unique PubNub client identifier. + * @param userId - Unique identifier of the user used by PubNub client instance. + */ + const invalidateClient = (subscriptionKey, clientId, userId) => { + delete pubNubClients[clientId]; + let clients = pubNubClientsBySubscriptionKey[subscriptionKey]; + if (clients) { + // Clean up linkage between client and subscription key. + clients = clients.filter((client) => client.clientIdentifier !== clientId); + if (clients.length > 0) + pubNubClientsBySubscriptionKey[subscriptionKey] = clients; + else + delete pubNubClientsBySubscriptionKey[subscriptionKey]; + // Clean up presence state information if not in use anymore. + if (clients.length === 0) + delete presenceState[subscriptionKey]; + // Clean up service workers client linkage to PubNub clients. + if (clients.length > 0) { + const workerClients = serviceWorkerClients[subscriptionKey]; + if (workerClients) { + delete workerClients[clientId]; + if (Object.keys(workerClients).length === 0) + delete serviceWorkerClients[subscriptionKey]; + } + } + else + delete serviceWorkerClients[subscriptionKey]; + } + }; /** * Validate received event payload. */ diff --git a/dist/web/pubnub.worker.min.js b/dist/web/pubnub.worker.min.js index 90002fb5b..864385802 100644 --- a/dist/web/pubnub.worker.min.js +++ b/dist/web/pubnub.worker.min.js @@ -1,2 +1,2 @@ !function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";function e(e,t,n,i){return new(n||(n=Promise))((function(s,r){function o(e){try{c(i.next(e))}catch(e){r(e)}}function u(e){try{c(i.throw(e))}catch(e){r(e)}}function c(e){var t;e.done?s(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,u)}c((i=i.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var n,i,s={exports:{}}; -/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */n=s,function(e){var t="0.1.0",n={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i};function i(){var e,t,n="";for(e=0;e<32;e++)t=16*Math.random()|0,8!==e&&12!==e&&16!==e&&20!==e||(n+="-"),n+=(12===e?4:16===e?3&t|8:t).toString(16);return n}function s(e,t){var i=n[t||"all"];return i&&i.test(e)||!1}i.isUUID=s,i.VERSION=t,e.uuid=i,e.isUUID=s}(i=s.exports),null!==n&&(n.exports=i.uuid);var r=t(s.exports),o={createUUID:()=>r.uuid?r.uuid():r()};const u=new TextDecoder,c=new Map,l={},a={},d={},p={},f={};self.addEventListener("activate",(e=>{e.waitUntil(self.clients.claim())})),self.addEventListener("message",(e=>{if(!x(e))return;const t=e.data;"send-request"===t.type?t.request.path.startsWith("/v2/subscribe")?(k(e),h(t)):(l[t.clientIdentifier]||k(e),b(e)):"cancel-request"===t.type&&v(t)}));const h=e=>{const t=j(e),n=l[e.clientIdentifier];n&&w("start",[n],(new Date).toISOString()),"string"!=typeof t?(e.request.cancellable&&c.set(t.identifier,new AbortController),g(t,(()=>q(t.identifier)),((e,n)=>{E(e,n),m(e,t.identifier)}),((e,n)=>{E(e,null,t,T(n)),m(e,t.identifier)}))):n&&(n.subscription.previousTimetoken=n.subscription.timetoken,n.subscription.timetoken=f[t].timetoken,n.subscription.serviceRequestId=t)},b=e=>{const t=e.data,n=O(t),i=l[t.clientIdentifier];if(i){if(!n){const n=(new TextEncoder).encode('{"status": 200, "action": "leave", "message": "OK", "service":"Presence"}'),i=new Headers({"Content-Type":'text/javascript; charset="UTF-8"',"Content-Length":"74"}),s=new Response(n,{status:200,headers:i}),r=S([s,n]);return r.url=`${t.request.origin}${t.request.path}`,r.clientIdentifier=t.clientIdentifier,r.identifier=t.request.identifier,void A(e.source.id,r)}g(n,(()=>[i]),((e,n)=>{E(e,n,t.request)}),((e,n)=>{E(e,null,t.request,T(n))}))}},v=e=>{const t=l[e.clientIdentifier],n=t?t.subscription.serviceRequestId:void 0;if(t&&n&&(delete t.subscription.serviceRequestId,delete t.subscription.request,0===q(n).length)){const e=c.get(n);c.delete(n),delete f[n],e&&e.abort()}},g=(t,n,i,s)=>{e(void 0,void 0,void 0,(function*(){var e;const r=(new Date).getTime();Promise.race([fetch(I(t),{signal:null===(e=c.get(t.identifier))||void 0===e?void 0:e.signal,keepalive:!0}),y(t.identifier,t.timeout)]).then((e=>e.arrayBuffer().then((t=>[e,t])))).then((e=>{const s=e[1].byteLength>0?e[1]:void 0,o=n();0!==o.length&&(w("end",o,(new Date).toISOString(),t,s,e[0].headers.get("Content-Type"),(new Date).getTime()-r),i(o,e))})).catch((e=>{const t=n();0!==t.length&&s(t,e)}))}))},y=(e,t)=>new Promise(((n,i)=>{const s=setTimeout((()=>{c.delete(e),clearTimeout(s),i(new Error("Request timeout"))}),1e3*t)})),q=e=>Object.values(l).filter((t=>void 0!==t&&t.subscription.serviceRequestId===e)),m=(e,t)=>{delete f[t],e.forEach((e=>{delete e.subscription.request,delete e.subscription.serviceRequestId}))},I=e=>{let t;const n=e.queryParameters;let i=e.path;if(e.headers){t={};for(const[n,i]of Object.entries(e.headers))t[n]=i}return n&&0!==Object.keys(n).length&&(i=`${i}?${U(n)}`),new Request(`${e.origin}${i}`,{method:e.method,headers:t,redirect:"follow"})},j=e=>{var t,n,i,s;const r=l[e.clientIdentifier],u=R(r.subscription.previousTimetoken,e),c=o.createUUID(),a=Object.assign({},e.request);if(u.length>1){const s=F(u,e);if(s)return s;const o=(null!==(t=d[r.subscriptionKey])&&void 0!==t?t:{})[r.userId],l={},p=new Set(r.subscription.channelGroups),h=new Set(r.subscription.channels);o&&r.subscription.objectsWithState.length&&r.subscription.objectsWithState.forEach((e=>{const t=o[e];t&&(l[e]=t)}));for(const e of u){const{subscription:t}=e;t.serviceRequestId||(t.channelGroups.forEach(p.add,p),t.channels.forEach(h.add,h),t.serviceRequestId=c,o&&t.objectsWithState.forEach((e=>{const t=o[e];t&&!l[e]&&(l[e]=t)})))}const b=null!==(n=f[c])&&void 0!==n?n:f[c]={requestId:c,timetoken:null!==(i=a.queryParameters.tt)&&void 0!==i?i:"0",channelGroups:[],channels:[]};if(h.size){b.channels=Array.from(h).sort();const e=a.path.split("/");e[4]=b.channels.join(","),a.path=e.join("/")}p.size&&(b.channelGroups=Array.from(p).sort(),a.queryParameters["channel-group"]=b.channelGroups.join(",")),Object.keys(l).length&&(a.queryParameters.state=JSON.stringify(l))}else f[c]={requestId:c,timetoken:null!==(s=a.queryParameters.tt)&&void 0!==s?s:"0",channelGroups:r.subscription.channelGroups,channels:r.subscription.channels};return r.subscription.serviceRequestId=c,a.identifier=c,a},O=e=>{const t=l[e.clientIdentifier],n=K(e);let i=G(e.request),s=$(e.request);const r=Object.assign({},e.request);if(t){const{subscription:e}=t;s.length&&(e.channels=e.channels.filter((e=>!s.includes(e)))),i.length&&(e.channelGroups=e.channelGroups.filter((e=>!i.includes(e))))}for(const t of n)t.clientIdentifier!==e.clientIdentifier&&(s.length&&(s=s.filter((e=>!t.subscription.channels.includes(e)))),i.length&&(i=i.filter((e=>!t.subscription.channelGroups.includes(e)))));if(0!==s.length||0!==i.length){if(s.length){const e=r.path.split("/");e[4]=s.join(","),r.path=e.join("/")}return i.length&&(r.queryParameters["channel-group"]=i.join(",")),r}},A=(e,t)=>{self.clients.get(e).then((e=>{e&&e.postMessage(t)}))},w=(e,t,n,i,s,r,o)=>{var c;if(0===t.length)return;const l=null!==(c=p[t[0].subscriptionKey])&&void 0!==c?c:{};let a;if("start"===e)a={type:"request-progress-start",clientIdentifier:"",url:"",timestamp:n};else{let e;s&&r&&(-1!==r.indexOf("text/javascript")||-1!==r.indexOf("application/json")||-1!==r.indexOf("text/plain")||-1!==r.indexOf("text/html"))&&(e=u.decode(s)),a={type:"request-progress-end",clientIdentifier:"",url:"",response:e,timestamp:n,duration:o}}t.forEach((e=>{const t=l[e.clientIdentifier],{request:n}=e.subscription,s=null!=n?n:i;e.logVerbosity&&t&&s&&A(t,Object.assign(Object.assign({},a),{clientIdentifier:e.clientIdentifier,url:`${s.origin}${s.path}`,query:s.queryParameters}))}))},E=(e,t,n,i)=>{var s;if(0===e.length)return;if(!i&&!t)return;const r=null!==(s=p[e[0].subscriptionKey])&&void 0!==s?s:{};!i&&t&&(i=t[0].status>=400?T(void 0,t):S(t)),e.forEach((e=>{const t=r[e.clientIdentifier],{request:s}=e.subscription,o=null!=s?s:n;t&&o&&A(t,Object.assign(Object.assign({},i),{clientIdentifier:e.clientIdentifier,identifier:o.identifier,url:`${o.origin}${o.path}`}))}))},S=e=>{var t;const[n,i]=e,s=i.byteLength>0?i:void 0,r=parseInt(null!==(t=n.headers.get("Content-Length"))&&void 0!==t?t:"0",10),o=n.headers.get("Content-Type"),u={};return n.headers.forEach(((e,t)=>u[t]=e.toLowerCase())),{type:"request-process-success",clientIdentifier:"",identifier:"",url:"",response:{contentLength:r,contentType:o,headers:u,status:n.status,body:s}}},T=(e,t)=>{if(t)return Object.assign(Object.assign({},S(t)),{type:"request-process-error"});let n="NETWORK_ISSUE",i="Unknown error",s="Error";return e&&e instanceof Error&&(i=e.message,s=e.name),"AbortError"===s?(i="Request aborted",n="ABORTED"):"Request timeout"===i&&(n="TIMEOUT"),{type:"request-process-error",clientIdentifier:"",identifier:"",url:"",error:{name:s,type:n,message:i}}},k=e=>{var t,n,i,s,r,o,u,c,f,h,b,v,g,y,q,m,I,j,O,A,w,E,S,T,k,x,F,R,K,P;const U=e.data,{clientIdentifier:W}=U,C=U.request.queryParameters;let D=l[W];if(D){const e=null!==(b=C["channel-group"])&&void 0!==b?b:"",t=null!==(v=C.state)&&void 0!==v?v:"";if(D.subscription.filterExpression=null!==(g=C["filter-expr"])&&void 0!==g?g:"",D.subscription.previousTimetoken=D.subscription.timetoken,D.subscription.timetoken=null!==(y=C.tt)&&void 0!==y?y:"0",D.lastAvailabilityCheck=(new Date).getTime(),D.subscription.request=U.request,D.authKey=null!==(q=C.auth)&&void 0!==q?q:"",D.userId=C.uuid,D.subscription.path!==U.request.path&&(D.subscription.path=U.request.path,D.subscription.channels=$(U.request)),D.subscription.channelGroupQuery!==e&&(D.subscription.channelGroupQuery=e,D.subscription.channelGroups=G(U.request)),t.length>0){const e=JSON.parse(t),n=null!==(I=(x=null!==(m=d[k=D.subscriptionKey])&&void 0!==m?m:d[k]={})[F=D.userId])&&void 0!==I?I:x[F]={};Object.entries(e).forEach((([e,t])=>n[e]=t));for(const t of D.subscription.objectsWithState)e[t]||delete n[t];D.subscription.objectsWithState=Object.keys(e)}else if(D.subscription.objectsWithState.length){const e=null!==(O=(K=null!==(j=d[R=D.subscriptionKey])&&void 0!==j?j:d[R]={})[P=D.userId])&&void 0!==O?O:K[P]={};for(const t of D.subscription.objectsWithState)delete e[t];D.subscription.objectsWithState=[]}}else{const b=!U.request.path.startsWith("/v2/subscribe"),v=b?"":null!==(t=C["channel-group"])&&void 0!==t?t:"",g=b?"":null!==(n=C.state)&&void 0!==n?n:"";if(D=l[W]={clientIdentifier:W,subscriptionKey:U.subscriptionKey,userId:C.uuid,authKey:null!==(i=C.auth)&&void 0!==i?i:"",logVerbosity:U.logVerbosity,lastAvailabilityCheck:(new Date).getTime(),subscription:{path:b?"":U.request.path,channelGroupQuery:b?"":v,channels:b?[]:$(U.request),channelGroups:b?[]:G(U.request),previousTimetoken:b?"0":null!==(s=C.tt)&&void 0!==s?s:"0",timetoken:b?"0":null!==(r=C.tt)&&void 0!==r?r:"0",request:b?void 0:U.request,objectsWithState:[],filterExpression:b?void 0:null!==(o=C["filter-expr"])&&void 0!==o?o:""}},!b&&g.length>0){const e=JSON.parse(g),t=null!==(c=(w=null!==(u=d[A=D.subscriptionKey])&&void 0!==u?u:d[A]={})[E=D.userId])&&void 0!==c?c:w[E]={};Object.entries(e).forEach((([e,n])=>t[e]=n)),D.subscription.objectsWithState=Object.keys(e)}const y=null!==(f=a[S=U.subscriptionKey])&&void 0!==f?f:a[S]=[];y.every((e=>e.clientIdentifier!==W))&&y.push(D),(null!==(h=p[T=U.subscriptionKey])&&void 0!==h?h:p[T]={})[W]=e.source.id}},x=e=>{if(!(e.source&&e.source instanceof Client))return!1;const t=e.data,{clientIdentifier:n,subscriptionKey:i,logVerbosity:s}=t;return void 0!==s&&"boolean"==typeof s&&(!(!n||"string"!=typeof n)&&!(!i||"string"!=typeof i))},F=(e,t)=>{var n;const i=null!==(n=t.request.queryParameters["channel-group"])&&void 0!==n?n:"",s=t.request.path;let r,o;for(const n of e){const{subscription:e}=n;if(e.serviceRequestId){if(e.path===s&&e.channelGroupQuery===i)return e.serviceRequestId;{const n=f[e.serviceRequestId];if(r||(r=G(t.request)),o||(o=$(t.request)),o.length&&!P(n.channels,o))continue;if(r.length&&!P(n.channelGroups,r))continue;return e.serviceRequestId}}}},R=(e,t)=>{var n,i,s;const r=t.request.queryParameters,o=null!==(n=r["filter-expr"])&&void 0!==n?n:"",u=null!==(i=r.auth)&&void 0!==i?i:"",c=r.uuid;return(null!==(s=a[t.subscriptionKey])&&void 0!==s?s:[]).filter((t=>t.userId===c&&t.authKey===u&&t.subscription.filterExpression===o&&("0"===e||"0"===t.subscription.previousTimetoken||t.subscription.previousTimetoken===e)))},K=e=>{var t,n;const i=e.request.queryParameters,s=null!==(t=i.auth)&&void 0!==t?t:"",r=i.uuid;return(null!==(n=a[e.subscriptionKey])&&void 0!==n?n:[]).filter((e=>e.userId===r&&e.authKey===s))},$=e=>{const t=e.path.split("/")[e.path.startsWith("/v2/subscribe/")?4:6];return","===t?[]:t.split(",").filter((e=>e.length>0))},G=e=>{var t;const n=null!==(t=e.queryParameters["channel-group"])&&void 0!==t?t:"";return 0===n.length?[]:n.split(",").filter((e=>e.length>0))},P=(e,t)=>{const n=new Set(e);return t.every(n.has,n)},U=e=>Object.keys(e).map((t=>{const n=e[t];return Array.isArray(n)?n.map((e=>`${t}=${W(e)}`)).join("&"):`${t}=${W(n)}`})).join("&"),W=e=>encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`))})); +/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */n=s,function(e){var t="0.1.0",n={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i};function i(){var e,t,n="";for(e=0;e<32;e++)t=16*Math.random()|0,8!==e&&12!==e&&16!==e&&20!==e||(n+="-"),n+=(12===e?4:16===e?3&t|8:t).toString(16);return n}function s(e,t){var i=n[t||"all"];return i&&i.test(e)||!1}i.isUUID=s,i.VERSION=t,e.uuid=i,e.isUUID=s}(i=s.exports),null!==n&&(n.exports=i.uuid);var r=t(s.exports),o={createUUID:()=>r.uuid?r.uuid():r()};const u=new TextDecoder,c=new Map,l={},a={},d={},p={},f={};self.addEventListener("activate",(e=>{e.waitUntil(self.clients.claim())})),self.addEventListener("message",(e=>{if(!K(e))return;const t=e.data;"send-request"===t.type?t.request.path.startsWith("/v2/subscribe")?(k(e),h(t)):(l[t.clientIdentifier]||k(e),b(e)):"cancel-request"===t.type&&v(t)}));const h=e=>{const t=j(e),n=l[e.clientIdentifier];n&&w("start",[n],(new Date).toISOString()),"string"!=typeof t?(e.request.cancellable&&c.set(t.identifier,new AbortController),g(t,(()=>q(t.identifier)),((e,n)=>{E(e,n),I(e,t.identifier)}),((e,n)=>{E(e,null,t,T(n)),I(e,t.identifier)}))):n&&(n.subscription.previousTimetoken=n.subscription.timetoken,n.subscription.timetoken=f[t].timetoken,n.subscription.serviceRequestId=t)},b=e=>{const t=e.data,n=O(t),i=l[t.clientIdentifier];if(i){if(!n){const n=(new TextEncoder).encode('{"status": 200, "action": "leave", "message": "OK", "service":"Presence"}'),s=new Headers({"Content-Type":'text/javascript; charset="UTF-8"',"Content-Length":"74"}),r=new Response(n,{status:200,headers:s}),o=S([r,n]);return o.url=`${t.request.origin}${t.request.path}`,o.clientIdentifier=t.clientIdentifier,o.identifier=t.request.identifier,void A(e.source.id,o).then((e=>{e&&x(i.subscriptionKey,i.clientIdentifier,i.userId)}))}g(n,(()=>[i]),((e,n)=>{E(e,n,t.request)}),((e,n)=>{E(e,null,t.request,T(n))}))}},v=e=>{const t=l[e.clientIdentifier],n=t?t.subscription.serviceRequestId:void 0;if(t&&n&&(delete t.subscription.serviceRequestId,delete t.subscription.request,0===q(n).length)){const e=c.get(n);c.delete(n),delete f[n],e&&e.abort()}},g=(t,n,i,s)=>{e(void 0,void 0,void 0,(function*(){var e;const r=(new Date).getTime();Promise.race([fetch(m(t),{signal:null===(e=c.get(t.identifier))||void 0===e?void 0:e.signal,keepalive:!0}),y(t.identifier,t.timeout)]).then((e=>e.arrayBuffer().then((t=>[e,t])))).then((e=>{const s=e[1].byteLength>0?e[1]:void 0,o=n();0!==o.length&&(w("end",o,(new Date).toISOString(),t,s,e[0].headers.get("Content-Type"),(new Date).getTime()-r),i(o,e))})).catch((e=>{const t=n();0!==t.length&&s(t,e)}))}))},y=(e,t)=>new Promise(((n,i)=>{const s=setTimeout((()=>{c.delete(e),clearTimeout(s),i(new Error("Request timeout"))}),1e3*t)})),q=e=>Object.values(l).filter((t=>void 0!==t&&t.subscription.serviceRequestId===e)),I=(e,t)=>{delete f[t],e.forEach((e=>{delete e.subscription.request,delete e.subscription.serviceRequestId}))},m=e=>{let t;const n=e.queryParameters;let i=e.path;if(e.headers){t={};for(const[n,i]of Object.entries(e.headers))t[n]=i}return n&&0!==Object.keys(n).length&&(i=`${i}?${W(n)}`),new Request(`${e.origin}${i}`,{method:e.method,headers:t,redirect:"follow"})},j=e=>{var t,n,i,s;const r=l[e.clientIdentifier],u=R(r.subscription.previousTimetoken,e),c=o.createUUID(),a=Object.assign({},e.request);if(u.length>1){const s=F(u,e);if(s)return s;const o=(null!==(t=d[r.subscriptionKey])&&void 0!==t?t:{})[r.userId],l={},p=new Set(r.subscription.channelGroups),h=new Set(r.subscription.channels);o&&r.subscription.objectsWithState.length&&r.subscription.objectsWithState.forEach((e=>{const t=o[e];t&&(l[e]=t)}));for(const e of u){const{subscription:t}=e;t.serviceRequestId||(t.channelGroups.forEach(p.add,p),t.channels.forEach(h.add,h),t.serviceRequestId=c,o&&t.objectsWithState.forEach((e=>{const t=o[e];t&&!l[e]&&(l[e]=t)})))}const b=null!==(n=f[c])&&void 0!==n?n:f[c]={requestId:c,timetoken:null!==(i=a.queryParameters.tt)&&void 0!==i?i:"0",channelGroups:[],channels:[]};if(h.size){b.channels=Array.from(h).sort();const e=a.path.split("/");e[4]=b.channels.join(","),a.path=e.join("/")}p.size&&(b.channelGroups=Array.from(p).sort(),a.queryParameters["channel-group"]=b.channelGroups.join(",")),Object.keys(l).length&&(a.queryParameters.state=JSON.stringify(l))}else f[c]={requestId:c,timetoken:null!==(s=a.queryParameters.tt)&&void 0!==s?s:"0",channelGroups:r.subscription.channelGroups,channels:r.subscription.channels};return r.subscription.serviceRequestId=c,a.identifier=c,a},O=e=>{const t=l[e.clientIdentifier],n=$(e);let i=P(e.request),s=G(e.request);const r=Object.assign({},e.request);if(t){const{subscription:e}=t;s.length&&(e.channels=e.channels.filter((e=>!s.includes(e)))),i.length&&(e.channelGroups=e.channelGroups.filter((e=>!i.includes(e))))}for(const t of n)t.clientIdentifier!==e.clientIdentifier&&(s.length&&(s=s.filter((e=>!t.subscription.channels.includes(e)))),i.length&&(i=i.filter((e=>!t.subscription.channelGroups.includes(e)))));if(0!==s.length||0!==i.length){if(s.length){const e=r.path.split("/");e[4]=s.join(","),r.path=e.join("/")}return i.length&&(r.queryParameters["channel-group"]=i.join(",")),r}},A=(e,t)=>self.clients.get(e).then((e=>!!e&&(e.postMessage(t),!0))),w=(e,t,n,i,s,r,o)=>{var c;if(0===t.length)return;const l=null!==(c=p[t[0].subscriptionKey])&&void 0!==c?c:{};let a;if("start"===e)a={type:"request-progress-start",clientIdentifier:"",url:"",timestamp:n};else{let e;s&&r&&(-1!==r.indexOf("text/javascript")||-1!==r.indexOf("application/json")||-1!==r.indexOf("text/plain")||-1!==r.indexOf("text/html"))&&(e=u.decode(s)),a={type:"request-progress-end",clientIdentifier:"",url:"",response:e,timestamp:n,duration:o}}t.forEach((e=>{const t=l[e.clientIdentifier],{request:n}=e.subscription,s=null!=n?n:i;e.logVerbosity&&t&&s&&A(t,Object.assign(Object.assign({},a),{clientIdentifier:e.clientIdentifier,url:`${s.origin}${s.path}`,query:s.queryParameters})).then((t=>{t&&x(e.subscriptionKey,e.clientIdentifier,e.userId)}))}))},E=(e,t,n,i)=>{var s;if(0===e.length)return;if(!i&&!t)return;const r=null!==(s=p[e[0].subscriptionKey])&&void 0!==s?s:{};!i&&t&&(i=t[0].status>=400?T(void 0,t):S(t)),e.forEach((e=>{const t=r[e.clientIdentifier],{request:s}=e.subscription,o=null!=s?s:n;t&&o&&A(t,Object.assign(Object.assign({},i),{clientIdentifier:e.clientIdentifier,identifier:o.identifier,url:`${o.origin}${o.path}`})).then((t=>{t&&x(e.subscriptionKey,e.clientIdentifier,e.userId)}))}))},S=e=>{var t;const[n,i]=e,s=i.byteLength>0?i:void 0,r=parseInt(null!==(t=n.headers.get("Content-Length"))&&void 0!==t?t:"0",10),o=n.headers.get("Content-Type"),u={};return n.headers.forEach(((e,t)=>u[t]=e.toLowerCase())),{type:"request-process-success",clientIdentifier:"",identifier:"",url:"",response:{contentLength:r,contentType:o,headers:u,status:n.status,body:s}}},T=(e,t)=>{if(t)return Object.assign(Object.assign({},S(t)),{type:"request-process-error"});let n="NETWORK_ISSUE",i="Unknown error",s="Error";return e&&e instanceof Error&&(i=e.message,s=e.name),"AbortError"===s?(i="Request aborted",n="ABORTED"):"Request timeout"===i&&(n="TIMEOUT"),{type:"request-process-error",clientIdentifier:"",identifier:"",url:"",error:{name:s,type:n,message:i}}},k=e=>{var t,n,i,s,r,o,u,c,f,h,b,v,g,y,q,I,m,j,O,A,w,E,S,T,k,x,K,F,R,$;const U=e.data,{clientIdentifier:W}=U,C=U.request.queryParameters;let D=l[W];if(D){const e=null!==(b=C["channel-group"])&&void 0!==b?b:"",t=null!==(v=C.state)&&void 0!==v?v:"";if(D.subscription.filterExpression=null!==(g=C["filter-expr"])&&void 0!==g?g:"",D.subscription.previousTimetoken=D.subscription.timetoken,D.subscription.timetoken=null!==(y=C.tt)&&void 0!==y?y:"0",D.subscription.request=U.request,D.authKey=null!==(q=C.auth)&&void 0!==q?q:"",D.userId=C.uuid,D.subscription.path!==U.request.path&&(D.subscription.path=U.request.path,D.subscription.channels=G(U.request)),D.subscription.channelGroupQuery!==e&&(D.subscription.channelGroupQuery=e,D.subscription.channelGroups=P(U.request)),t.length>0){const e=JSON.parse(t),n=null!==(m=(x=null!==(I=d[k=D.subscriptionKey])&&void 0!==I?I:d[k]={})[K=D.userId])&&void 0!==m?m:x[K]={};Object.entries(e).forEach((([e,t])=>n[e]=t));for(const t of D.subscription.objectsWithState)e[t]||delete n[t];D.subscription.objectsWithState=Object.keys(e)}else if(D.subscription.objectsWithState.length){const e=null!==(O=(R=null!==(j=d[F=D.subscriptionKey])&&void 0!==j?j:d[F]={})[$=D.userId])&&void 0!==O?O:R[$]={};for(const t of D.subscription.objectsWithState)delete e[t];D.subscription.objectsWithState=[]}}else{const b=!U.request.path.startsWith("/v2/subscribe"),v=b?"":null!==(t=C["channel-group"])&&void 0!==t?t:"",g=b?"":null!==(n=C.state)&&void 0!==n?n:"";if(D=l[W]={clientIdentifier:W,subscriptionKey:U.subscriptionKey,userId:C.uuid,authKey:null!==(i=C.auth)&&void 0!==i?i:"",logVerbosity:U.logVerbosity,subscription:{path:b?"":U.request.path,channelGroupQuery:b?"":v,channels:b?[]:G(U.request),channelGroups:b?[]:P(U.request),previousTimetoken:b?"0":null!==(s=C.tt)&&void 0!==s?s:"0",timetoken:b?"0":null!==(r=C.tt)&&void 0!==r?r:"0",request:b?void 0:U.request,objectsWithState:[],filterExpression:b?void 0:null!==(o=C["filter-expr"])&&void 0!==o?o:""}},!b&&g.length>0){const e=JSON.parse(g),t=null!==(c=(w=null!==(u=d[A=D.subscriptionKey])&&void 0!==u?u:d[A]={})[E=D.userId])&&void 0!==c?c:w[E]={};Object.entries(e).forEach((([e,n])=>t[e]=n)),D.subscription.objectsWithState=Object.keys(e)}const y=null!==(f=a[S=U.subscriptionKey])&&void 0!==f?f:a[S]=[];y.every((e=>e.clientIdentifier!==W))&&y.push(D),(null!==(h=p[T=U.subscriptionKey])&&void 0!==h?h:p[T]={})[W]=e.source.id}},x=(e,t,n)=>{delete l[t];let i=a[e];if(i)if(i=i.filter((e=>e.clientIdentifier!==t)),i.length>0?a[e]=i:delete a[e],0===i.length&&delete d[e],i.length>0){const n=p[e];n&&(delete n[t],0===Object.keys(n).length&&delete p[e])}else delete p[e]},K=e=>{if(!(e.source&&e.source instanceof Client))return!1;const t=e.data,{clientIdentifier:n,subscriptionKey:i,logVerbosity:s}=t;return void 0!==s&&"boolean"==typeof s&&(!(!n||"string"!=typeof n)&&!(!i||"string"!=typeof i))},F=(e,t)=>{var n;const i=null!==(n=t.request.queryParameters["channel-group"])&&void 0!==n?n:"",s=t.request.path;let r,o;for(const n of e){const{subscription:e}=n;if(e.serviceRequestId){if(e.path===s&&e.channelGroupQuery===i)return e.serviceRequestId;{const n=f[e.serviceRequestId];if(r||(r=P(t.request)),o||(o=G(t.request)),o.length&&!U(n.channels,o))continue;if(r.length&&!U(n.channelGroups,r))continue;return e.serviceRequestId}}}},R=(e,t)=>{var n,i,s;const r=t.request.queryParameters,o=null!==(n=r["filter-expr"])&&void 0!==n?n:"",u=null!==(i=r.auth)&&void 0!==i?i:"",c=r.uuid;return(null!==(s=a[t.subscriptionKey])&&void 0!==s?s:[]).filter((t=>t.userId===c&&t.authKey===u&&t.subscription.filterExpression===o&&("0"===e||"0"===t.subscription.previousTimetoken||t.subscription.previousTimetoken===e)))},$=e=>{var t,n;const i=e.request.queryParameters,s=null!==(t=i.auth)&&void 0!==t?t:"",r=i.uuid;return(null!==(n=a[e.subscriptionKey])&&void 0!==n?n:[]).filter((e=>e.userId===r&&e.authKey===s))},G=e=>{const t=e.path.split("/")[e.path.startsWith("/v2/subscribe/")?4:6];return","===t?[]:t.split(",").filter((e=>e.length>0))},P=e=>{var t;const n=null!==(t=e.queryParameters["channel-group"])&&void 0!==t?t:"";return 0===n.length?[]:n.split(",").filter((e=>e.length>0))},U=(e,t)=>{const n=new Set(e);return t.every(n.has,n)},W=e=>Object.keys(e).map((t=>{const n=e[t];return Array.isArray(n)?n.map((e=>`${t}=${C(e)}`)).join("&"):`${t}=${C(n)}`})).join("&"),C=e=>encodeURIComponent(e).replace(/[!~*'()]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`))})); diff --git a/src/core/components/uuid.js b/src/core/components/uuid.ts similarity index 72% rename from src/core/components/uuid.js rename to src/core/components/uuid.ts index a22f987f7..023de232a 100644 --- a/src/core/components/uuid.js +++ b/src/core/components/uuid.ts @@ -5,6 +5,7 @@ export default { if (uuidGenerator.uuid) { return uuidGenerator.uuid(); } + // @ts-expect-error Depending on module type it may be callable. return uuidGenerator(); }, }; diff --git a/src/event-engine/core/retryPolicy.ts b/src/event-engine/core/retryPolicy.ts index 9d85d5d65..ebc515ad9 100644 --- a/src/event-engine/core/retryPolicy.ts +++ b/src/event-engine/core/retryPolicy.ts @@ -7,7 +7,6 @@ export class RetryPolicy { return { delay: configuration.delay, maximumRetry: configuration.maximumRetry, - // TODO: Find out actual `error` type. /* eslint-disable @typescript-eslint/no-explicit-any */ shouldRetry(error: any, attempt: number) { if (error?.status?.statusCode === 403) { @@ -19,7 +18,6 @@ export class RetryPolicy { const delay = reason.retryAfter ?? this.delay; return (delay + Math.random()) * 1000; }, - // TODO: Find out actual `error` type. /* eslint-disable @typescript-eslint/no-explicit-any */ getGiveupReason(error: any, attempt: number) { if (this.maximumRetry <= attempt) { diff --git a/src/event-engine/presence/dispatcher.ts b/src/event-engine/presence/dispatcher.ts index 5366ae873..f2dfa0571 100644 --- a/src/event-engine/presence/dispatcher.ts +++ b/src/event-engine/presence/dispatcher.ts @@ -20,7 +20,6 @@ export type Dependencies = { config: PrivateClientConfiguration; presenceState: Record; - // TODO: Find out actual `status` type. /* eslint-disable @typescript-eslint/no-explicit-any */ emitStatus: (status: any) => void; }; diff --git a/src/event-engine/presence/effects.ts b/src/event-engine/presence/effects.ts index 3f7f21b1e..0eddde4d5 100644 --- a/src/event-engine/presence/effects.ts +++ b/src/event-engine/presence/effects.ts @@ -12,7 +12,6 @@ export const leave = createEffect('LEAVE', (channels: string[], groups: string[] groups, })); -// TODO: Find out actual `status` type. /* eslint-disable @typescript-eslint/no-explicit-any */ export const emitStatus = createEffect('EMIT_STATUS', (status: any) => status); diff --git a/src/transport/service-worker/subscription-service-worker.ts b/src/transport/service-worker/subscription-service-worker.ts index 397a9289b..970ded65a 100644 --- a/src/transport/service-worker/subscription-service-worker.ts +++ b/src/transport/service-worker/subscription-service-worker.ts @@ -284,11 +284,6 @@ type PubNubClientState = { */ logVerbosity: boolean; - /** - * Last time, PubNub client instance responded to the `ping` event. - */ - lastAvailabilityCheck: number; - /** * Current subscription session information. * @@ -518,6 +513,11 @@ const handleSendSubscribeRequestEvent = (event: SendRequestEvent) => { ); }; +/** + * Handle client request to leave request. + * + * @param event - Leave event details. + */ const handleSendLeaveRequestEvent = (event: ExtendableMessageEvent) => { const data = event.data as SendRequestEvent; const request = leaveTransportRequestFromEvent(data); @@ -533,7 +533,9 @@ const handleSendLeaveRequestEvent = (event: ExtendableMessageEvent) => { result.clientIdentifier = data.clientIdentifier; result.identifier = data.request.identifier; - publishClientEvent((event.source! as Client).id, result); + publishClientEvent((event.source! as Client).id, result).then((sent) => { + if (sent) invalidateClient(client.subscriptionKey, client.clientIdentifier, client.userId); + }); return; } @@ -867,9 +869,11 @@ const leaveTransportRequestFromEvent = (event: SendRequestEvent): TransportReque * @param event - Service worker event object. */ const publishClientEvent = (identifier: string, event: ServiceWorkerEvent) => { - self.clients.get(identifier).then((client) => { - if (!client) return; + return self.clients.get(identifier).then((client) => { + if (!client) return false; + client.postMessage(event); + return true; }); }; @@ -939,6 +943,8 @@ const notifyRequestProcessing = ( clientIdentifier: client.clientIdentifier, url: `${decidedRequest.origin}${decidedRequest.path}`, query: decidedRequest.queryParameters, + }).then((sent) => { + if (sent) invalidateClient(client.subscriptionKey, client.clientIdentifier, client.userId); }); } }); @@ -982,6 +988,8 @@ const notifyRequestProcessingResult = ( clientIdentifier: client.clientIdentifier, identifier: decidedRequest.identifier, url: `${decidedRequest.origin}${decidedRequest.path}`, + }).then((sent) => { + if (sent) invalidateClient(client.subscriptionKey, client.clientIdentifier, client.userId); }); } }); @@ -1099,7 +1107,6 @@ const registerClientIfRequired = (event: ExtendableMessageEvent) => { userId: query.uuid as string, authKey: (query.auth ?? '') as string, logVerbosity: information.logVerbosity, - lastAvailabilityCheck: new Date().getTime(), subscription: { path: !isPresenceLeave ? information.request.path : '', channelGroupQuery: !isPresenceLeave ? channelGroupQuery : '', @@ -1134,7 +1141,6 @@ const registerClientIfRequired = (event: ExtendableMessageEvent) => { client.subscription.filterExpression = (query['filter-expr'] ?? '') as string; client.subscription.previousTimetoken = client.subscription.timetoken; client.subscription.timetoken = (query.tt ?? '0') as string; - client.lastAvailabilityCheck = new Date().getTime(); client.subscription.request = information.request; client.authKey = (query.auth ?? '') as string; client.userId = query.uuid as string; @@ -1169,6 +1175,39 @@ const registerClientIfRequired = (event: ExtendableMessageEvent) => { } }; +/** + * Clean up resources used by registered PubNub client instance. + * + * @param subscriptionKey - Subscription key which has been used by the + * invalidated instance. + * @param clientId - Unique PubNub client identifier. + * @param userId - Unique identifier of the user used by PubNub client instance. + */ +const invalidateClient = (subscriptionKey: string, clientId: string, userId: string) => { + delete pubNubClients[clientId]; + let clients = pubNubClientsBySubscriptionKey[subscriptionKey]; + + if (clients) { + // Clean up linkage between client and subscription key. + clients = clients.filter((client) => client.clientIdentifier !== clientId); + if (clients.length > 0) pubNubClientsBySubscriptionKey[subscriptionKey] = clients; + else delete pubNubClientsBySubscriptionKey[subscriptionKey]; + + // Clean up presence state information if not in use anymore. + if (clients.length === 0) delete presenceState[subscriptionKey]; + + // Clean up service workers client linkage to PubNub clients. + if (clients.length > 0) { + const workerClients = serviceWorkerClients[subscriptionKey]; + if (workerClients) { + delete workerClients[clientId]; + + if (Object.keys(workerClients).length === 0) delete serviceWorkerClients[subscriptionKey]; + } + } else delete serviceWorkerClients[subscriptionKey]; + } +}; + /** * Validate received event payload. */