Skip to content

Commit

Permalink
$mol_offline refactor, disable cache on page reload via dev server
Browse files Browse the repository at this point in the history
  • Loading branch information
zerkalica committed Oct 25, 2024
1 parent 48f2d26 commit b22b11a
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 103 deletions.
1 change: 1 addition & 0 deletions build/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function $mol_build_client() {

socket.onmessage = message => {
if( message.data !== '$mol_build_obsolete' ) return
sessionStorage.setItem('$mol_build_obsolete', '1')
location.reload()
}

Expand Down
2 changes: 1 addition & 1 deletion offline/install/install.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace $ {
try {
$mol_offline()
new $mol_offline().run()
} catch( error ) {
console.error( error )
}
Expand Down
6 changes: 5 additions & 1 deletion offline/offline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
namespace $ {

export function $mol_offline( ) {}
export class $mol_offline {
run() {
return false
}
}

}
293 changes: 192 additions & 101 deletions offline/offline.web.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,200 @@
namespace $ {

const blacklist = new Set([
'//cse.google.com/adsense/search/async-ads.js'
])

/** Installs service worker proxy, which caches all requests and respond from cache on http errors. */
export function $mol_offline_web() {

if( typeof window === 'undefined' ) {

interface FetchEvent extends Event {
request: Request
respondWith(response: Response | Promise<Response | null> | null): void
waitUntil(promise: Promise<unknown>): void
}

export class $mol_offline_web extends $mol_offline {
web_js() { return 'web.js' }

blacklist = new Set([
'//cse.google.com/adsense/search/async-ads.js'
])

obsolete_key() { return '$mol_build_obsolete' }

in_worker() { return typeof window === 'undefined' }

is_supported() {
if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) {
console.warn( 'HTTPS or localhost is required for service workers.' )
return false
}

self.addEventListener( 'install' , ( event : any )=> {
;( self as any ).skipWaiting()
} )

self.addEventListener( 'activate' , ( event : any )=> {

// caches.delete( '$mol_offline' )

;( self as any ).clients.claim()

$$.$mol_log3_done({
place: '$mol_offline',
message: 'Activated',
})

} )

self.addEventListener( 'fetch' , ( event : any )=> {

const request = event.request as Request

if( blacklist.has( request.url.replace( /^https?:/, '' ) ) ) {
return event.respondWith(
new Response(
null,
{
status: 418,
statusText: 'Blocked'
},
)
)
}

if( request.method !== 'GET' ) return
if( !/^https?:/.test( request.url ) ) return
if( /\?/.test( request.url ) ) return
if( request.cache === 'no-store' ) return

const fetch_data = () => fetch( request ).then( response => {
if (response.status !== 200) return response
event.waitUntil(
caches.open( '$mol_offline' ).then(
cache => cache.put( request , response )
)
)

return response.clone()
} )

const fresh = request.cache === 'force-cache' ? null : fetch_data()

if (fresh) event.waitUntil( fresh )

event.respondWith(
caches.match( request ).then(
cached => request.cache === 'no-cache' || request.cache === 'reload'
? ( cached
? fresh!
.then(actual => {
if (actual.status === cached.status) return actual
throw new Error(
`${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`,
{ cause: actual }
)
})
.catch((err: Error) => {
const cloned = cached.clone()
const message = `${err.cause instanceof Response ? '' : '500 '}${err.message} $mol_offline fallback to cache`
cloned.headers.set( '$mol_offline_remote_status', message )
return cloned
})
: fresh
)
: ( cached || fresh || fetch_data() )
if( ! navigator.serviceWorker ) {
console.warn( 'Service Worker is not supported.' )
return false
}

return true
}

protected _registration = null as null | Promise<ServiceWorkerRegistration>

registration() {
if (this._registration) return this._registration
if ( this.in_worker() ) return null
if ( ! this.is_supported() ) return null

navigator.serviceWorker.register(this.web_js())

return this._registration = navigator.serviceWorker.ready
}

async notify() {
const reg = await this.registration()

const key = this.obsolete_key()
const ignore_cache = sessionStorage.getItem(key)
sessionStorage.removeItem(key)

if (ignore_cache) reg?.active?.postMessage({ message: key })
}

override run() {
if (! this.registration()) {
this.worker()
return false
}

this.notify()

// const reg = await this.registration()
// reg?.addEventListener( 'updatefound', ()=> {
// const worker = reg.installing!
// worker.addEventListener( 'statechange', ()=> {
// if( worker.state !== 'activated' ) return
// window.location.reload()
// } )
// } )

return true
}

_worker = null as null | {
skipWaiting(): void
clients: {
claim(): void
}
addEventListener(name: string, cb: (e: Event) => void): void
}

worker() {
if (this._worker) return this._worker
const worker = this._worker = self as unknown as NonNullable<typeof this['_worker']>
worker.addEventListener( 'beforeinstallprompt' , this.beforeinstallprompt.bind(this) )
worker.addEventListener( 'install' , this.install.bind(this))
worker.addEventListener( 'activate' , this.activate.bind(this))
worker.addEventListener( 'message', this.message.bind(this))
worker.addEventListener( 'fetch', this.fetch_event.bind(this) as any)

return worker
}

message(event: unknown) {
if (! event || typeof event !== 'object') return

const message = (event as { data: { message?: string }}).data?.message ?? ''

if (! message) return
if (message === this.obsolete_key()) return this.build_obsolete()

}

beforeinstallprompt(event: Event & { prompt?(): void }) {
event.prompt?.()
}

install(event: Event) { this.worker().skipWaiting() }

activate(event: Event) {
// caches.delete( '$mol_offline' )

this.worker().clients.claim()

$$.$mol_log3_done({
place: '$mol_offline',
message: 'Activated',
})
}

protected ignore_cache = false

build_obsolete() { this.ignore_cache = true }

fetch_event(event: FetchEvent) {
const request = event.request

if( this.blacklist.has( request.url.replace( /^https?:/, '' ) ) ) {
return event.respondWith(
new Response(
null,
{
status: 418,
statusText: 'Blocked'
},
)
)

})
}

if( request.method !== 'GET' ) return
if( !/^https?:/.test( request.url ) ) return
if( /\?/.test( request.url ) ) return
if( request.cache === 'no-store' ) return

const response = this.respond(event)
event.waitUntil( response )
event.respondWith( response )
}

async respond(event: FetchEvent) {
const request = event.request
const fresh = request.cache === 'force-cache' ? null : this.fetch_data(event)

const cached = this.ignore_cache ? null : await caches.match( request )

if (request.cache !== 'no-cache' && request.cache !== 'reload') {
return cached || fresh || this.fetch_data(event)
}

if ( ! cached || ! fresh) return fresh

try {
const actual = await fresh
if (actual.status === cached.status) return actual

throw new Error(
`${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`,
{ cause: actual }
)

} catch (err) {
const cloned = cached.clone()
const message = `${(err as Error).cause instanceof Response ? '' : '500 '}${
(err as Error).message} $mol_offline fallback to cache`

cloned.headers.set( '$mol_offline_remote_status', message )

return cloned
}
}

async put_cache(request: Request, response: Response | Promise<Response>) {
const cache = await caches.open( '$mol_offline' )
return cache.put( request , await response )
}

async fetch_data (event: FetchEvent) {
const request = event.request
const response = await fetch( request )
if (response.status !== 200) return response

const cached = this.put_cache(request, response)
event.waitUntil(cached)

self.addEventListener( 'beforeinstallprompt' , ( event : any )=> event.prompt() )

} else if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) {
console.warn( 'HTTPS or localhost is required for service workers.' )
} else if( !navigator.serviceWorker ) {
console.warn( 'Service Worker is not supported.' )
} else {
navigator.serviceWorker.register( 'web.js' ).then( reg => {
// reg.addEventListener( 'updatefound', ()=> {
// const worker = reg.installing!
// worker.addEventListener( 'statechange', ()=> {
// if( worker.state !== 'activated' ) return
// window.location.reload()
// } )
// } )
} )
return response.clone()
}

}
Expand Down

0 comments on commit b22b11a

Please sign in to comment.