WebStorage re-write using native Cache

This commit is contained in:
2025-01-26 16:11:39 +00:00
parent f10da029ba
commit 37250b3925
5 changed files with 244 additions and 131 deletions
+178
View File
@@ -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",
+1
View File
@@ -6,6 +6,7 @@
"dev": "wrangler dev --ip=0.0.0.0"
},
"devDependencies": {
"@twurple/auth": "^7.2.1",
"wrangler": "^3.60.3"
}
}
+65
View File
@@ -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)
}
)
}
}
-37
View File
@@ -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<Response>}
*/
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
})
}
-94
View File
@@ -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<this.storage.length;i++){
const path=this.storage.key(i)
const data=await this.storage.getItem(path)
const url=this.#getURL(path)
let promise=fetch(url,{method:'POST',headers:await this.#getHeaders(),data:data,keepalive:true})
promises.push(promise)
}
return Promise.all(promises)
}
}