diff --git a/dom/listener/listener.ts b/dom/listener/listener.ts index 5463a1da001..cf085cd8285 100644 --- a/dom/listener/listener.ts +++ b/dom/listener/listener.ts @@ -1,19 +1,25 @@ namespace $ { - export class $mol_dom_listener extends $mol_object { + + type Source = { + addEventListener(name: string, handler: (event: EventType) => unknown, passive?: any ): unknown + removeEventListener(name: string, handler: (event: EventType) => unknown, passive?: any ): unknown + } + + export class $mol_dom_listener extends $mol_object { constructor( - public _node : any , - public _event : string , - public _handler : ( event : any )=> any , - public _config : boolean|{ passive : boolean } = { passive : true } + protected node : Source, + protected event : string , + protected handler : (event: EventType) => unknown , + protected config : boolean | { passive : boolean } = { passive : true } ) { super() - this._node.addEventListener( this._event , this._handler , this._config ) + this.node.addEventListener( this.event , this.handler , this.config ) } destructor() { - this._node.removeEventListener( this._event , this._handler , this._config ) + this.node.removeEventListener( this.event , this.handler , this.config ) super.destructor() } diff --git a/embed/native/native.view.ts b/embed/native/native.view.ts index 9fc9abbd8f5..fd6f7494990 100644 --- a/embed/native/native.view.ts +++ b/embed/native/native.view.ts @@ -33,7 +33,7 @@ namespace $.$$ { @ $mol_mem message_listener() { - return new $mol_dom_listener( + return new $mol_dom_listener>( $mol_dom_context, 'message', $mol_wire_async( this ).message_receive diff --git a/fail/catch/catch.ts b/fail/catch/catch.ts index 428a97b7b0f..b8ed371654d 100644 --- a/fail/catch/catch.ts +++ b/fail/catch/catch.ts @@ -2,7 +2,7 @@ namespace $ { const catched = new WeakMap< any , boolean >() - export function $mol_fail_catch( error: unknown ) { + export function $mol_fail_catch( error: unknown ): error is Error { if( typeof error !== 'object' ) return false if( $mol_promise_like( error ) ) $mol_fail_hidden( error ) diff --git a/fetch/fetch.ts b/fetch/fetch.ts index 126cf87afba..76cdcc7c4cb 100644 --- a/fetch/fetch.ts +++ b/fetch/fetch.ts @@ -6,6 +6,11 @@ namespace $ { super() } + @ $mol_action + clone() { + return new this.$.$mol_fetch_response(new Response(this.native.body, this.native)) + } + status() { const types = [ 'unknown', 'inform', 'success', 'redirect', 'wrong', 'failed' ] as const return types[ Math.floor( this.native.status / 100 ) ] diff --git a/notify/notify.meta.tree b/notify/notify.meta.tree deleted file mode 100644 index 49489eb16b2..00000000000 --- a/notify/notify.meta.tree +++ /dev/null @@ -1 +0,0 @@ -include \/mol/offline/install diff --git a/notify/notify.node.ts b/notify/notify.node.ts deleted file mode 100644 index 71d3fb651ae..00000000000 --- a/notify/notify.node.ts +++ /dev/null @@ -1,20 +0,0 @@ -namespace $ { - - export class $mol_notify { - - @ $mol_mem - static allowed( next?: boolean ) { - return false - } - - @ $mol_action - static show( info: { - context: string, - message: string, - uri: string - } ) { - } - - } - -} diff --git a/notify/notify.ts b/notify/notify.ts new file mode 100644 index 00000000000..555bc74d784 --- /dev/null +++ b/notify/notify.ts @@ -0,0 +1,31 @@ +namespace $ { + + export type $mol_notify_info = { + context: string, + message: string, + uri: string + } + + export class $mol_notify extends $mol_object { + + @ $mol_mem + static allowed( next?: boolean ) { + return false + } + + static show( info: $mol_notify_info ) { + this.$.$mol_service_worker.rpc().$mol_notify_service.show(info) + } + + } + + export class $mol_notify_service extends $mol_service_plugin_notify { + static show(info: $mol_notify_info) {} + + } + + export namespace $mol_service_plugin { + export let $mol_notify_service = $.$mol_notify_service + } + +} diff --git a/notify/notify.web.ts b/notify/notify.web.ts index c3c16435671..f598e2b8999 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -1,12 +1,13 @@ namespace $ { /** Manages system notifications. Notifications of same context are auto joined to one notification. */ - export class $mol_notify { + export class $mol_notify_web extends $mol_notify { @ $mol_mem - static allowed( next?: boolean ) { + static override allowed( next?: boolean ) { - let perm = Notification.permission + + let perm = this.$.$mol_dom_context.Notification.permission if( next === undefined ) return perm === 'granted' if( perm === 'granted' ) return true @@ -16,33 +17,22 @@ namespace $ { return perm === 'granted' } - static async request_permissions() { + static request_permissions() { return new Promise< NotificationPermission >( done => - Notification.requestPermission( perm => { + this.$.$mol_dom_context.Notification.requestPermission( perm => { done( perm ) } ) ) } - @ $mol_action - static show( info: { - context: string, - message: string, - uri: string - } ) { - navigator.serviceWorker.controller!.postMessage( info ) - } - } - - if( typeof window === 'undefined' ) { - - self.addEventListener( 'message', async event => { - - let { context: title, message: body, uri: data } = event.data + + $.$mol_notify = $mol_notify_web + + export class $mol_notify_service_web extends $mol_notify_service { + static override show({ context: title, message: body, uri: data }: $mol_notify_info) { const tag = data - - const existen = await $mol_service().getNotifications({ tag }) + const existen = this.$.$mol_service_self_web.notifications({ tag }) for( const not of existen ) { @@ -54,29 +44,27 @@ namespace $ { // const vibrate = [ 100, 200, 300, 400, 500 ] - await $mol_service().showNotification( title, { body, data, /*vibrate,*/ tag } ) - - } ) - - self.addEventListener( 'notificationclick', $mol_service_handler( async ( event: any )=> { - - const clients: any[] = await ( self as any ).clients.matchAll({ includeUncontrolled: true }) - event.notification.close() + this.$.$mol_service_self_web.notification_show( title, { body, data, /*vibrate,*/ tag } ) - if( clients.length ) { - - const last = clients[ clients.length - 1 ] - await last.focus() - await last.navigate( event.notification.data ) - - } else { - - await ( self as any ).clients.openWindow( event.notification.data ) - + } + + static override notification( notification: Notification ) { + const matched = this.$.$mol_service_self_web.clients_grab({ includeUncontrolled: true, type: 'window' }) + const last = matched.at(-1) + + if( last ) { + last.focus() + last.navigate( notification.data ) + + return null } - - } ) ) - + + this.$.$mol_service_self_web.window_open( notification.data ) + return null + } } - + + $.$mol_notify_service = $mol_notify_service_web + + $mol_service_plugin.$mol_notify_service = $mol_notify_service_web } diff --git a/offline/install/install.ts b/offline/install/install.ts index b6ce77c459b..2cc8af51adb 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,7 +1,3 @@ namespace $ { - try { - $mol_offline() - } catch( error ) { - console.error( error ) - } + $mol_offline } diff --git a/offline/offline.ts b/offline/offline.ts index 179d65834e7..e2efbefd34d 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,5 +1,69 @@ namespace $ { - export function $mol_offline( ) {} + export class $mol_offline extends $mol_service_plugin_cache { + static blocked_urls = [ + '//cse.google.com/adsense/search/async-ads.js' + ] + static override blocked( request: Request ) { + const normalized_url = request.url.replace( /^https?:/, '' ) + + return this.blocked_urls.includes(normalized_url) + } + + static override activate() { + this.$.$mol_service_worker.claim() + } + + static override need_modify(request: Request) { + if( request.method !== 'GET' ) return false + if( !/^https?:/.test( request.url ) ) return false + if( /\?/.test( request.url ) ) return false + if( request.cache === 'no-store' ) return false + + return true + } + + static override modify(request: Request) { + let fallback_header + + const html = request.mode === 'navigate' + const cache = request.cache + + if (cache === 'reload' || ( cache === 'no-cache' && ! html ) ) { + if (cache === 'reload') { + // F5 + Disable cache + request = new ($mol_wire_sync(Request))(request, { cache: 'no-cache' }) + } + + // fetch with fallback to cache if statuses not match + try { + const actual = this.$.$mol_fetch.response(request) + if (actual.code() < 400) return actual.native + + fallback_header = actual.message() + } catch (err) { + if ( $mol_promise_like(err) ) $mol_fail_hidden(err) + fallback_header = (err as Error).message || 'Fetch error' + } + } + + if (cache !== 'force-cache') { + request = new ($mol_wire_sync(Request))(request, { cache: 'force-cache' }) + } + + const cached = this.$.$mol_fetch.response(request) + + if (! fallback_header ) return cached.native + + const clone = cached.clone() + clone.headers().set( '$mol_offline_remote_status', `${fallback_header} $mol_offline fallback to cache`) + + return clone.native + } + } + + export namespace $mol_service_plugin { + export let $mol_offline = $.$mol_offline + } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 2b1d160cc46..4ad7cdd08a1 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,113 +1,15 @@ 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' ) { - - 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() ) - ) - ) - - }) - - 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() - // } ) - // } ) - } ) + export class $mol_offline_web extends $mol_offline { + @ $mol_mem + static prompt() { + this.$.$mol_offline_prompt.last()?.prompt() } - } $.$mol_offline = $mol_offline_web + $mol_service_plugin.$mol_offline = $.$mol_offline_web + if ( $mol_service_process()) { + Promise.resolve().then(() => $mol_wire_async($mol_offline_web).prompt()) + } } diff --git a/offline/prompt/prompt.web.ts b/offline/prompt/prompt.web.ts new file mode 100644 index 00000000000..24b6f95780e --- /dev/null +++ b/offline/prompt/prompt.web.ts @@ -0,0 +1,39 @@ +namespace $ { + // https://web.dev/learn/pwa/installation-prompt + interface BeforeInstallEvent extends Event { + prompt(): Promise + platforms: readonly string[] + userChoise: Promise<{ + outcome: 'accepted' | 'dismissed' + }> + } + + export class $mol_offline_prompt extends $mol_object { + @ $mol_mem + protected static event() { + const install = (event: Event) => { + this.last(new this.$.$mol_offline_prompt(event as BeforeInstallEvent)) + } + + const win = this.$.$mol_dom_context + win.addEventListener( 'beforeinstallprompt' , install) + + return { + destructor: () => win.removeEventListener('beforeinstallprompt', install) + } + } + + @ $mol_mem + static last(next?: $mol_offline_prompt) { + this.event() + return next ?? null + } + + constructor(readonly native: BeforeInstallEvent) { super() } + + prompt() { return $mol_wire_sync(this.native).prompt() } + choise_async() { return this.native.userChoise } + choise() { return $mol_wire_sync(this).choise_async() } + } + +} diff --git a/rpc/client/client.ts b/rpc/client/client.ts new file mode 100644 index 00000000000..af03bdb02fa --- /dev/null +++ b/rpc/client/client.ts @@ -0,0 +1,57 @@ +namespace $ { + export class $mol_rpc_client< Handlers extends Record unknown>> extends $mol_object { + @ $mol_action + protected create_id() { + return `$mol_rpc_client:${this.$.$mol_guid()}:${ Date.now().toString(16) }` + } + + call_async(id: string) { + return new Promise( ( done , fail )=> { + const timer = setTimeout( + () => handle({ data: { id, error: 'RPC call timeout' } }), + this.timeout() + ) + + const handle = ( event : { data: unknown } )=> { + const data = event.data as { id?: string, error?: string, result?: string } + if (! data || typeof data !== 'object') return + if( data.id !== id ) return + + clearTimeout(timer) + + this.$.$mol_dom_context.removeEventListener( 'message' , handle ) + + if( data.error ) fail( new Error( data.error ) ) + else done( data.result ) + } + + this.$.$mol_dom_context.addEventListener( 'message' , handle ) + } ) + } + + @ $mol_action + post_message(request: { id: string, name: string, args: unknown[] }) { + return null + } + + @ $mol_action + call( request : { name : string , args : unknown[] } ) { + const id = this.create_id() + this.post_message({ ...request, id }) + return $mol_wire_sync(this).call_async(id) + } + + timeout() { return 10000 } + + @ $mol_mem + proxy() { + return new Proxy( {} as Handlers, { + get : ( target : unknown , name : string )=> { + return ( ... args : unknown[] )=> this.call({ name , args }) + } + } ) + } + + + } +} diff --git a/rpc/client/frame/frame.ts b/rpc/client/frame/frame.ts index 5135e014e78..dc9905cf077 100644 --- a/rpc/client/frame/frame.ts +++ b/rpc/client/frame/frame.ts @@ -1,59 +1,37 @@ namespace $ { - export class $mol_rpc_client_frame< Handlers > extends $mol_object { + export class $mol_rpc_client_frame< Handlers extends {}> extends $mol_rpc_client { @ $mol_mem_key - static item< Handlers >( uri : string ) { - return this.make({ uri : $mol_const( uri ) }) as $mol_rpc_client_frame< Handlers > + static item< Handlers extends {}>( uri : string ): $mol_rpc_client_frame< Handlers > { + return this.make({ uri : $mol_const( uri ) }) } uri() { return '' } - @ $mol_mem - frame() { - return $mol_fiber_sync( ()=> new Promise< HTMLIFrameElement >( ( done , fail )=> { + frame_async() { + return new Promise< HTMLIFrameElement >( ( done , fail )=> { const frame = this.$.$mol_dom_context.document.createElement( 'iframe' ) frame.src = this.uri() - frame.onload = $mol_fiber_root( event => done( frame ) ) + frame.onload = () => done( frame ) frame.style.display = 'none' this.$.$mol_dom_context.document.documentElement.appendChild( frame ) - } ) ) () + } ) } - @ $mol_action - call( { name , args } : { name : string , args : any[] } ) { - - const frame = this.frame() - return $mol_fiber_sync( ()=> new Promise( ( done , fail )=> { - - const id = `$mol_rpc_client_frame:${ Date.now().toString(16) }` - - frame.contentWindow!.postMessage( { id , name , args } , '*' ) - - const handle = $mol_fiber_root( ( event : MessageEvent )=> { - if( event.data.id !== id ) return - - this.$.$mol_dom_context.removeEventListener( 'message' , handle ) - - if( event.data.error ) fail( new Error( event.data.error ) ) - else done( event.data.result ) - } ) - - this.$.$mol_dom_context.addEventListener( 'message' , handle ) - - } ) ) () - + @ $mol_mem + protected frame() { + $mol_wire_solid() + this.uri() + return $mol_wire_sync(this).frame_async() } - @ $mol_mem - proxy() { - return new Proxy( {} , { - get : ( target : any , name : string )=> { - return ( ... args : any[] )=> this.call({ name , args }) - } - } ) + @ $mol_action + override post_message(request: { id: string, name: string, args: unknown[] }) { + this.frame().contentWindow!.postMessage( request , '*' ) + return null } } diff --git a/rpc/server/iframe/iframe.ts b/rpc/server/iframe/iframe.ts new file mode 100644 index 00000000000..f6ad8481647 --- /dev/null +++ b/rpc/server/iframe/iframe.ts @@ -0,0 +1,11 @@ +namespace $ { + + export class $mol_rpc_server_iframe extends $mol_rpc_server { + protected override message(event: MessageEvent) { $mol_wire_async(this).send(event) } + send( event: MessageEvent ) { + return this.$.$mol_dom_context.parent.postMessage( this.response(event), '*' ) ?? null + } + + } + +} diff --git a/rpc/server/server.ts b/rpc/server/server.ts index 0926dee3be1..8c23f1e4994 100644 --- a/rpc/server/server.ts +++ b/rpc/server/server.ts @@ -1,34 +1,43 @@ namespace $ { - export class $mol_rpc_server extends $mol_object { - + export class $mol_rpc_server extends $mol_object { @ $mol_mem listener() { - return new this.$.$mol_dom_listener( + return new this.$.$mol_dom_listener( this.$.$mol_dom_context , 'message' , - event => $mol_wire_async(this).handle(event.data), + event => this.request(event) ? this.message(event) : null ) } + protected message(event: Message) { } + + handlers(): Record unknown> { + return {} + } + + request(event: Message) { + return event.data && typeof event.data === 'object' && 'id' in event.data + ? event.data as { id : string , name : string , args : readonly unknown[] } + : null + } + @ $mol_action - handle( { id , name , args } : { id : string , name : string , args : any[] } ) { + response(event: Message) { + const { id, name, args } = this.request(event)! - const handler = this.handlers()[ name ] || this.handlers()[ '' ] - if( !handler ) return + const response = { id, result: undefined as unknown, error: undefined as Error | undefined } try { - const result = handler( ... args ) - this.$.$mol_dom_context.parent.postMessage( { id , result } , '*' ) - } catch( error: any ) { - if( error instanceof Promise ) $mol_fail_hidden( error ) - this.$.$mol_dom_context.parent.postMessage( { id , error : error.message } , '*' ) - } + const handler = this.handlers()[ name ] || this.handlers()[ '' ] - } + if( ! handler ) throw new Error('No handler ' + name) + response.result = handler( ... args ) + } catch( err ) { + if ( $mol_fail_catch(err) ) response.error = err + } - handlers(): Record { - return {} + return response } } diff --git a/rpc/server/worker/worker.ts b/rpc/server/worker/worker.ts new file mode 100644 index 00000000000..ac76beee864 --- /dev/null +++ b/rpc/server/worker/worker.ts @@ -0,0 +1,14 @@ +namespace $ { + + export class $mol_rpc_server_worker extends $mol_rpc_server { + protected override message(event: ExtendableMessageEvent) { + event.waitUntil($mol_wire_async(this).send(event)) + } + + send( event: ExtendableMessageEvent ) { + return event.source!.postMessage( this.response(event) ) ?? null + } + + } + +} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts new file mode 100644 index 00000000000..4b85e09619a --- /dev/null +++ b/service/plugin/plugin.ts @@ -0,0 +1,37 @@ +namespace $ { + if ( $mol_service_process() ) { + Promise.resolve().then(() => $mol_service_self.init()) + } else { + $mol_service_worker.init() + } + + export namespace $mol_service_plugin { + let _ + } + + export class $mol_service_plugin_base extends $mol_object { + + static is( + this: This, + some: { prototype: unknown } + ): some is This { + return some.prototype instanceof this + } + + static install() { } + static activate() { } + } + + export class $mol_service_plugin_cache extends $mol_service_plugin_base { + static blocked(request: Request) { return false } + static need_modify(request: Request) { return false } + static modify(request: Request) { return new Response } + } + + export class $mol_service_plugin_notify extends $mol_service_plugin_base { + static notification(e: unknown) { } + static notification_close(e: unknown) { } + static push(data: null | { json() : {}}) {} + } + +} diff --git a/service/process/process.ts b/service/process/process.ts new file mode 100644 index 00000000000..76d78b9a09e --- /dev/null +++ b/service/process/process.ts @@ -0,0 +1,5 @@ +namespace $ { + export function $mol_service_process() { + return typeof self !== 'undefined' && Boolean((self as unknown as { serviceWorker?: {} }).serviceWorker) + } +} diff --git a/service/self/self.ts b/service/self/self.ts new file mode 100644 index 00000000000..f95277f76c9 --- /dev/null +++ b/service/self/self.ts @@ -0,0 +1,21 @@ +namespace $ { + /** + * Со стороны + */ + export class $mol_service_self extends $mol_object { + static init() {} + + static claim() {} + + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) + } + } + +} diff --git a/service/self/self.web.ts b/service/self/self.web.ts new file mode 100644 index 00000000000..c45b695307f --- /dev/null +++ b/service/self/self.web.ts @@ -0,0 +1,225 @@ +namespace $ { + export class $mol_service_self_web extends $mol_service_self { + static handle): void }>( cb: (e: E) => void ) { + + return $mol_func_name_from((event: E) => { + event.waitUntil($mol_wire_async(cb)(event)) + }, cb) + + } + + @ $mol_mem + static override init() { + try { + this.scope() + this.rpc_server().listener() + this.rpc_server().handlers() + } catch (error) { + $mol_fail_log( error ) + } + } + + @ $mol_mem + protected static scope() { + const scope = self as unknown as ServiceWorkerGlobalScope + + const handlers = { + install: this.handle(this.install.bind(this)), + activate: this.handle(this.activate.bind(this)), + + messageerror: this.message_error.bind(this), + + notificationclick: this.handle(this.notify.bind(this, 'notification')), + notificationclose: this.handle(this.notify.bind(this, 'notification_close')), + push: this.handle(this.push.bind(this)), + + fetch: this.fetch.bind(this), + } as const + + Object.entries(handlers).forEach(([name, cb]) => { + scope.addEventListener(name, cb as (e: Event) => void) + }) + + return Object.assign(scope, { + destructor: () => { + Object.entries(handlers).forEach(([name, cb]) => { + scope.removeEventListener(name, cb as (e: Event) => void) + }) + } + }) + } + + protected static reg() { return this.scope().registration } + + static preload() { return $mol_wire_sync(this.reg().navigationPreload) } + static pushes() { return $mol_wire_sync(this.reg().pushManager) } + + static notifications(params?: GetNotificationOptions) { + return $mol_wire_sync(this.reg()).getNotifications(params) + } + + static notification_show(title: string, options?: NotificationOptions) { + return $mol_wire_sync(this.reg()).showNotification(title, options) + } + + protected static clients() { + return $mol_wire_sync(this.scope().clients) + } + + static claim() { return this.clients().claim() } + + @ $mol_mem_key + static client(id: string, next?: Cli) { + const client = next ?? (this.clients().get(id) as Cli) + return client ? $mol_wire_sync(client) : null + } + + @ $mol_action + static clients_grab(query?: Query) { + return this.clients().matchAll(query) + .map(client => this.client(client.id, client as Query['type'] extends 'window' ? WindowClient : Client)!) + } + + static window_open(url: string | URL) { + return this.clients().openWindow(url) + } + + protected static message_error(event: MessageEvent) { + this.$.$mol_log3_fail({ + place: `${this}.message_error()`, + message: 'Deserialization failed', + data: event.data + }) + } + + @ $mol_mem + static rpc_server() { + return this.$.$mol_rpc_server_worker.make({ + handlers: () => this.handlers() + }) + } + + @ $mol_mem + static handlers() { + return new Proxy({}, { + get: (t, name) => { + const [klass, method] = (name as string).split('.') ?? ['', ''] + const plugin = this.$.$mol_service_plugin[klass as keyof typeof $mol_service_plugin] + + return (...args: unknown[] ) => (plugin as any)[method as keyof typeof plugin](...args) + } + }) + } + + protected static plugin_error(error: unknown, place: string) { + if ($mol_promise_like(error)) { + error = new Error('Promise not allowed', { cause: error }) + } + + this.$.$mol_log3_fail({ + place, + message: (error as Error).message, + error, + }) + } + + protected static install(event: ExtendableEvent) { + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting#examples + // https://github.com/GoogleChrome/samples/blob/master/service-worker/prefetch-video/service-worker.js#L46 + this.scope().skipWaiting() + + for (const plugin of this.plugins()) { + try { + plugin.install() + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.install()`) + } + } + } + + protected static activate(event: ExtendableEvent) { + for (const plugin of this.plugins()) { + try { + plugin.activate() + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.activate()`) + } + } + + this.$.$mol_log3_done({ + place: `${this}.activate()`, + message: 'Activated', + }) + } + + @ $mol_mem + protected static plugins_raw() { + return Object.values(this.$.$mol_service_plugin) as readonly { prototype: unknown }[] + } + + protected static plugins() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_base.is(plugin)) + } + + protected static plugins_notify() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_notify.is(plugin)) + } + + protected static plugins_cache() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_cache.is(plugin)) + } + + protected static push(event: PushEvent) { + for (const plugin of this.plugins_notify()) { + try { + plugin.push(event.data) + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.push()`) + } + } + } + + protected static notify(method: 'notification' | 'notification_close', event: NotificationEvent) { + for (const plugin of this.plugins_notify()) { + try { + plugin[method](event.notification) + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.${method}()`) + } + } + } + + protected static fetch(event: FetchEvent) { + let target + const request = event.request + + try { + target = this.plugins_cache().find(plugin => plugin.blocked(request)) + if (target) return event.respondWith(this.blocked_response()) + + target = this.plugins_cache().find(plugin => plugin.need_modify(request)) + if (! target) return null + + } catch (error) { + this.plugin_error(error, `${target}.blocked()`) + + return null + } + + const promise = $mol_wire_async(target).modify(request) + .catch(error => { + this.plugin_error(error, `${target}.modify()`) + throw error + }) + event.respondWith(promise) + + return null + } + } + + $.$mol_service_self = $mol_service_self_web +} diff --git a/service/service.ts b/service/service.ts deleted file mode 100644 index ed4a8bcea6e..00000000000 --- a/service/service.ts +++ /dev/null @@ -1,21 +0,0 @@ -namespace $ { - - function worker() { - return navigator.serviceWorker.ready - } - - export function $mol_service() { - if( typeof window === 'undefined' ) { - return ( self as any ).registration as ServiceWorkerRegistration - } else { - return $mol_wire_sync( worker )() - } - } - - export function $mol_service_handler< E extends Event >( handle : ( event: E )=> Promise ) { - return ( event: E )=> { - ;( event as any ).waitUntil( handle( event ) ) - } - } - -} diff --git a/service/worker/worker.ts b/service/worker/worker.ts new file mode 100644 index 00000000000..3ea292ea698 --- /dev/null +++ b/service/worker/worker.ts @@ -0,0 +1,63 @@ +namespace $ { + /** + * Со стороны + */ + export class $mol_service_worker extends $mol_object { + static path() { return 'web.js' } + + static send_timeout() { return 20000 } + + protected static post_message(data: {}) { + } + + @ $mol_mem + static rpc() { + $mol_wire_solid() + + const rpc = this.$.$mol_rpc_client.make>({ + timeout: () => this.send_timeout(), + post_message: request => this.post_message(request) ?? null + }) + + return new Proxy({} as typeof $mol_service_plugin, { + get: (t, klass) => { + return new Proxy({} as any, { + get: (t, method) => { + return (...args: unknown[]) => rpc.call({ + name: `${klass as string}.${method as string}`, + args + }) + } + }) + } + }) + } + + @ $mol_mem + static init() { + try { + this.state() + // this.rpc() + } catch (error) { + $mol_fail_log( error ) + } + } + + static state() { + return 'installing' as 'activated' | 'activating' | 'installed' | 'installing' | 'parsed' | 'redundant' + } + + static claim() {} + + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) + } + } + +} diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts new file mode 100644 index 00000000000..29e88955606 --- /dev/null +++ b/service/worker/worker.web.ts @@ -0,0 +1,80 @@ +/// + +namespace $ { + + export class $mol_service_worker_web extends $mol_service_worker { + + @ $mol_mem + protected static container() { + if ( this.$.$mol_service_process()) throw new Error('Not for running in service worker') + const win = this.$.$mol_dom_context + + if( ! win.navigator.serviceWorker ) { + throw new Error('Service Worker is not supported.') + } + if( win.location.protocol !== 'https:' && win.location.hostname !== 'localhost' ) { + throw new Error( 'HTTPS or localhost is required for service workers.' ) + } + + return $mol_wire_sync(win.navigator.serviceWorker) + } + + @ $mol_mem + protected static reg() { + const reg = this.container().register( this.path() ) + + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/updatefound_event + const worker_reset = () => this.worker(null) + reg.addEventListener( 'updatefound', worker_reset) + + return $mol_wire_sync(Object.assign(reg, { + destructor: () => { + reg.removeEventListener( 'updatefound', worker_reset) + } + })) + } + + static update() { return this.reg().update() } + static unregister() { return this.reg().unregister() } + static update_status() { return this.reg().updateViaCache } + static scope() { return this.reg().scope } + + static preload() { return $mol_wire_sync(this.reg().navigationPreload) } + static pushes() { return $mol_wire_sync(this.reg().pushManager) } + + static notifications(params?: GetNotificationOptions) { + return $mol_wire_sync(this.reg()).getNotifications(params) + } + + static notification_show(title: string, options?: NotificationOptions) { + return $mol_wire_sync(this.reg()).showNotification(title, options) + } + + @ $mol_mem + protected static worker(reset?: null) { + const reg = this.reg() + const worker = reg.installing ?? reg.waiting ?? reg.active + if (! worker) throw new Error('Not for running in service worker') + + const state_reset = () => this.state(null) + worker.addEventListener( 'statechange', state_reset) + + return $mol_wire_sync(Object.assign(worker, { + destructor: () => worker.removeEventListener('statechange', state_reset) + })) + } + + @ $mol_mem + static override state(reset?: null) { return this.worker().state } + + static ready_async() { return this.container().ready } + + protected static override post_message(data: {}) { + $mol_wire_sync( this ).ready_async().active!.postMessage(data) + } + + } + + $.$mol_service_worker = $mol_service_worker_web + +}