diff --git a/package-lock.json b/package-lock.json index c6f8f91..a6b9d2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "twitch-cloud-ebs", "version": "1.0.0", "devDependencies": { + "@twurple/auth": "^7.2.1", "wrangler": "^3.60.3" } }, @@ -122,6 +123,77 @@ "node": ">=12" } }, + "node_modules/@d-fischer/cross-fetch": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.5.tgz", + "integrity": "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/@d-fischer/detect-node": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/detect-node/-/detect-node-3.0.1.tgz", + "integrity": "sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@d-fischer/logger": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@d-fischer/logger/-/logger-4.2.3.tgz", + "integrity": "sha512-mJUx9OgjrNVLQa4od/+bqnmD164VTCKnK5B4WOW8TX5y/3w2i58p+PMRE45gUuFjk2BVtOZUg55JQM3d619fdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/shared-utils": "^3.2.0", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/qs": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@d-fischer/qs/-/qs-7.0.2.tgz", + "integrity": "sha512-yAu3xDooiL+ef84Jo8nLjDjWBRk7RXk163Y6aTvRB7FauYd3spQD/dWvgT7R4CrN54Juhrrc3dMY7mc+jZGurQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/shared-utils": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@d-fischer/shared-utils/-/shared-utils-3.6.4.tgz", + "integrity": "sha512-BPkVLHfn2Lbyo/ENDBwtEB8JVQ+9OzkjJhUunLaxkw4k59YFlQxUUwlDBejVSFcpQT0t+D3CQlX+ySZnQj0wxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/typed-event-emitter": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@d-fischer/typed-event-emitter/-/typed-event-emitter-3.3.3.tgz", + "integrity": "sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, "node_modules/@esbuild-plugins/node-globals-polyfill": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", @@ -558,6 +630,56 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@twurple/api-call": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.2.1.tgz", + "integrity": "sha512-x+p6fMleFA/pJHVu8tFpkDkARGGt30EAUA+cLc+dEtU1EpH1mv7nv5LLHh9NW9vdZpYp0jk5TyY/TSbHdj7Ggw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@d-fischer/cross-fetch": "^5.0.1", + "@d-fischer/qs": "^7.0.2", + "@d-fischer/shared-utils": "^3.6.1", + "@twurple/common": "7.2.1", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/auth": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@twurple/auth/-/auth-7.2.1.tgz", + "integrity": "sha512-IGHsJ/g0RblRSBC59XKA9277rE5w1dAKdhCPQz1QwkWnt6fsUyVoD1ut+qZvjuh7ssX1SBtNzFqohydfmWZytg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.2.1", + "@d-fischer/shared-utils": "^3.6.1", + "@d-fischer/typed-event-emitter": "^3.3.1", + "@twurple/api-call": "7.2.1", + "@twurple/common": "7.2.1", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/common": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.2.1.tgz", + "integrity": "sha512-veLY5laA00dPuekAcpHN6GDvnr6s1uH7yBDebLC1NhpcLkQgiAlGA50RdNS5YeA3xz7kfE2MOcYSi2S6Qoc7EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.6.1", + "klona": "^2.0.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -765,6 +887,16 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -852,6 +984,27 @@ "mustache": "bin/mustache" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/ohash": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", @@ -972,6 +1125,13 @@ "npm": ">=6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1014,6 +1174,24 @@ "ufo": "^1.5.4" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/workerd": { "version": "1.20241230.0", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241230.0.tgz", diff --git a/package.json b/package.json index 1e6f8fa..3f4eb98 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dev": "wrangler dev --ip=0.0.0.0" }, "devDependencies": { + "@twurple/auth": "^7.2.1", "wrangler": "^3.60.3" } } diff --git a/static/WebStorage.mjs b/static/WebStorage.mjs new file mode 100644 index 0000000..cd3a351 --- /dev/null +++ b/static/WebStorage.mjs @@ -0,0 +1,65 @@ +export default class WebStorage { + + #origin=new URL(import.meta.url).origin + /** @type {import('@twurple/auth').AuthProvider} */ + #auth_provider = null; + /** @type {Cache} */ + #cache = null + + /** + * + * @param {import('@twurple/auth').AuthProvider} auth_provider + */ + constructor(auth_provider) { + this.#auth_provider = auth_provider + auth_provider.getAccessTokenForUser() + .then(token => caches.open(token.userId + '/' + auth_provider.clientId)) + .then(cache => this.#cache = cache) + } + + /** + * + * @param {String} resource + * @param {RequestInit} options + */ + async fetch(resource, options) { + const token = await this.#auth_provider.getAccessTokenForUser() + if (!options.headers) { + options.headers = {} + } + options.headers['authorization'] = 'OAuth ' + token.accessToken + resource = new URL(resource, this.#origin) + if ('method' in options) { + switch (options.method) { + case 'PUT': + case 'POST': { + const response = await fetch(resource, options) + if (!response.ok) { + return response + } + const blob = await new Request(resource, options).blob() + this.#cache.put(resource, new Response(blob)) + return response + } + case 'DELETE': { + const response = await fetch(resource, options) + if (!response.ok) { + return response + } + this.#cache.delete(resource) + return response + } + } + } + return fetch(resource, options).then( + response => { + this.#cache.put(resource, response) + return response + }, + error => { + console.warn(error) + return this.#cache.match(resource) + } + ) + } +} \ No newline at end of file diff --git a/static/fetch.mjs b/static/fetch.mjs deleted file mode 100644 index c5d5e98..0000000 --- a/static/fetch.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import localforage from 'https://cdn.jsdelivr.net/npm/localforage/+esm' -/** @type {Storage} */ -const cache=localforage.createInstance({name:import.meta.url}) -// make sure we don't lose the bultin fetch -const fetch=window.fetch - -/** - * `fetch` with an infinite cache - * @param {RequestInfo} resource - * @param {RequestInit} options - * @returns {Promise} - */ -export default async function fetchCached(resource,options={}){ - if('body' in options){ - cache.setItem(resource.toString(),options.body) - return fetch(resource,options) - } - if('method' in options && options.method==='DELETE'){ - cache.removeItem(resource.toString()) - } - return fetch(resource,options).then(async response=>{ - if(!response.ok){ - throw response - } - cache.setItem(resource,await response.clone().blob()) - return response - }).catch(async error=>{ - const body=await cache.getItem(resource.toString()) - if(body){ - return new Response(body) - } - if(error instanceof Response){ - return error - } - throw error - }) -} \ No newline at end of file diff --git a/static/storage.mjs b/static/storage.mjs deleted file mode 100644 index d02296b..0000000 --- a/static/storage.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import localforage from 'https://cdn.jsdelivr.net/npm/localforage/+esm' - -export default class WebStorage { - constructor(authProvider,userID,isPublic=false){ - this.authProvider=authProvider - this.userID=userID - let prefix='private.' - if(isPublic){ - prefix='public.' - } - /** @type {Storage} */ - this.storage=localforage.createInstance({name:prefix+authProvider.clientId+'.'+import.meta.url}) - } - - #getURL(path){ - const url=new URL(path,import.meta.url) - url.searchParams.append('public',this.isPublic) - return url - } - - async #getHeaders(){ - const token=await this.authProvider.getAccessTokenForUser(this.userID) - return {authorization:'OAuth '+token.accessToken} - } - - get length(){ - return this.storage.length - } - - key(n){ - return this.storage.key(n) - } - - async getItem(path){ - const url=this.#getURL(path) - - return fetch(url,{headers:await this.#getHeaders()}).then(async response=>{ - if(!response.ok){ - throw 'failed to fetch' - }else{ - response.clone().blob().then(blob=>{ - this.storage.setItem(path,blob) - }) - return response - } - }).catch(async ()=>{ - const data=await this.storage.getItem(path) - if(!data){ - return new Response(null,{status:404}) - }else{ - return new Response(data) - } - }) - } - - async setItem(path,data){ - const url=this.#getURL(path) - - this.storage.setItem(path,data) - return fetch(url,{method:'PUT',body:data,headers:await this.#getHeaders(),keepalive:true}) - } - - async removeItem(path){ - const url=this.#getURL(path) - - this.storage.removeItem(path)//TODO the server is recursive, but this library is not - return fetch(url,{method:'DELETE',headers:await this.#getHeaders(),keepalive:true}) - } - - async clear(){ - this.clearCache() - - const url=this.#getURL('/') - return fetch(url,{method:'DELETE',headers:await this.#getHeaders(),keepalive:true}) - } - - clearCache(){ - this.storage.clear() - } - - async sync(){ - await fetch(this.#getURL('/'),{method:'DELETE',headers:await this.#getHeaders()}) - const promises=[] - for(let i;i