@socketsupply/socket
Version:
A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.
498 lines (421 loc) • 14.3 kB
JavaScript
/* eslint-disable import/first */
globalThis.isServiceWorkerScope = true
import { ServiceWorkerGlobalScope } from './global.js'
import { createStorageInterface } from './storage.js'
import { Module, createRequire } from '../module.js'
import { createCallSites } from '../internal/callsite.js'
import { STATUS_CODES } from '../http.js'
import { Notification } from '../notification.js'
import { Environment } from './env.js'
import { Deferred } from '../async.js'
import { Buffer } from '../buffer.js'
import { Cache } from '../commonjs/cache.js'
import globals from '../internal/globals.js'
import process from '../process.js'
import clients from './clients.js'
import debug from './debug.js'
import hooks from '../hooks.js'
import state from './state.js'
import path from '../path.js'
import util from '../util.js'
import ipc from '../ipc.js'
import {
ExtendableMessageEvent,
NotificationEvent,
ExtendableEvent,
FetchEvent
} from './events.js'
import '../console.js'
Object.defineProperties(
globalThis,
Object.getOwnPropertyDescriptors(ServiceWorkerGlobalScope.prototype)
)
export default null
export const SERVICE_WORKER_READY_TOKEN = { __service_worker_ready: true }
export const module = { exports: {} }
export const events = new Set()
export const stages = { register: null, install: null, activate: null }
// service worker life cycle stages
stages.register = new Deferred()
stages.install = new Deferred(stages.register)
stages.activate = new Deferred(stages.install)
// event listeners
hooks.onReady(onReady)
globalThis.addEventListener('message', onMessage)
// service worker globals
globals.register('ServiceWorker.state', state)
globals.register('ServiceWorker.stages', stages)
globals.register('ServiceWorker.events', events)
globals.register('ServiceWorker.module', module)
let protocolData = null
export function onReady () {
globalThis.postMessage(SERVICE_WORKER_READY_TOKEN)
}
export async function onMessage (event) {
if (event instanceof ExtendableMessageEvent) {
return
}
const { data } = event
if (data?.register) {
event.stopImmediatePropagation()
const { id, scope, scriptURL } = data.register
const url = new URL(scriptURL)
if (!url.pathname.startsWith('/socket/')) {
// preload commonjs cache for user space server workers
Cache.restore(['loader.status', 'loader.response'])
}
state.id = id
state.serviceWorker.id = id
state.serviceWorker.scope = scope
state.serviceWorker.scriptURL = scriptURL
Module.main.addEventListener('error', (event) => {
if (event.error) {
debug(event.error)
}
})
Object.defineProperties(globalThis, {
require: {
configurable: false,
enumerable: false,
writable: false,
value: createRequire(scriptURL)
},
origin: {
configurable: false,
enumerable: true,
writable: false,
value: url.origin
},
__dirname: {
configurable: false,
enumerable: false,
writable: false,
value: path.dirname(url.pathname)
},
__filename: {
configurable: false,
enumerable: false,
writable: false,
value: url.pathname
},
module: {
configurable: false,
enumerable: false,
writable: false,
value: module
},
exports: {
configurable: false,
enumerable: false,
get: () => module.exports
},
process: {
configurable: false,
enumerable: false,
get: () => process
},
Buffer: {
configurable: false,
enumerable: false,
get: () => Buffer
},
global: {
configurable: false,
enumerable: false,
get: () => globalThis
}
})
// create global registration from construct
globalThis.registration = data.register
try {
// define the actual location of the worker. not `blob:...`
globalThis.RUNTIME_WORKER_LOCATION = scriptURL
// update and notify initial state change
state.serviceWorker.state = 'registering'
await state.notify('serviceWorker')
// open envirnoment
await Environment.open({ id, scope })
// install storage interfaces
Object.defineProperties(globalThis, {
localStorage: {
configurable: false,
enumerable: false,
writable: false,
value: await createStorageInterface('localStorage')
},
sessionStorage: {
configurable: false,
enumerable: false,
writable: false,
value: await createStorageInterface('sessionStorage')
},
memoryStorage: {
configurable: false,
enumerable: false,
writable: false,
value: await createStorageInterface('memoryStorage')
}
})
// import module, which could be ESM, CommonJS,
// or a simple ServiceWorker
const result = await import(scriptURL)
if (typeof module.exports === 'function') {
module.exports = {
default: module.exports
}
} else {
Object.assign(module.exports, result)
}
state.serviceWorker.state = 'registered'
await state.notify('serviceWorker')
} catch (err) {
debug(err)
state.serviceWorker.state = 'error'
await state.notify('serviceWorker')
return
}
if (module.exports.default && typeof module.exports.default === 'object') {
if (typeof module.exports.default.fetch === 'function') {
state.fetch = module.exports.default.fetch.bind(module.exports.default)
}
} else if (typeof module.exports.default === 'function') {
state.fetch = module.exports.default
} else if (typeof module.exports.fetch === 'function') {
state.fetch = module.exports.fetch.bind(module.exports)
}
if (module.exports.default && typeof module.exports.default === 'object') {
if (typeof module.exports.default.install === 'function') {
state.install = module.exports.default.install.bind(module.exports.default)
}
} else if (typeof module.exports.install === 'function') {
state.install = module.exports.install.bind(module.exports)
}
if (module.exports.default && typeof module.exports.default === 'object') {
if (typeof module.exports.default.activate === 'function') {
state.activate = module.exports.default.activate.bind(module.exports.default)
}
} else if (typeof module.exports.activate === 'function') {
state.activate = module.exports.activate.bind(module.exports)
}
if (module.exports.default && typeof module.exports.default === 'object') {
if (typeof module.exports.default.reportError === 'function') {
state.reportError = module.exports.default.reportError.bind(module.exports.default)
}
} else if (typeof module.exports.reportError === 'function') {
state.reportError = module.reportError.bind(module.exports)
}
if (typeof state.activate === 'function') {
globalThis.addEventListener('activate', async (event) => {
try {
const promise = state.activate(event.context.env, event.ontext)
event.waitUntil(promise)
await promise
} catch (err) {
debug(err)
}
})
}
if (typeof state.install === 'function') {
globalThis.addEventListener('install', async (event) => {
try {
const promise = state.install(event.context.env, event.context)
event.waitUntil(promise)
await promise
} catch (err) {
debug(err)
}
})
}
if (typeof state.fetch === 'function') {
if (!state.install) {
globalThis.addEventListener('install', () => {
globalThis.skipWaiting()
})
}
if (!state.activate) {
globalThis.addEventListener('activate', () => {
clients.claim()
})
}
globalThis.addEventListener('fetch', async (event) => {
const deferred = new Deferred()
let response = null
event.respondWith(deferred.promise)
try {
const promise = state.fetch(
event.request,
event.context.env,
event.context
)
event.waitUntil(promise)
response = await promise
} catch (err) {
debug(err)
if (event.request.headers.get('accept') === 'application/json') {
const stack = createCallSites(err, err.stack)
response = Response.json({ name: err.name, message: err.message, stack }, {
statusText: err.message || STATUS_CODES[500],
status: 500,
headers: {
'Runtime-Preload-Injection': 'disabled'
}
})
} else {
response = new Response(util.inspect(err), {
statusText: err.message || STATUS_CODES[500],
status: 500,
headers: {
'Runtime-Preload-Injection': 'disabled'
}
})
}
}
if (response) {
if (!response.statusText) {
response.statusText = STATUS_CODES[response.status]
}
deferred.resolve(response)
} else {
deferred.resolve(new Response('Not Found', {
statusText: STATUS_CODES[404],
status: 404,
headers: {
'Runtime-Preload-Injection': 'disabled'
}
}))
}
})
}
globalThis.postMessage({ __service_worker_registered: { id } })
return stages.register.resolve()
}
if (data?.unregister) {
event.stopImmediatePropagation()
state.serviceWorker.state = 'none'
await state.notify('serviceWorker')
globalThis.close()
return
}
if (data?.install?.id === state.id) {
event.stopImmediatePropagation()
await stages.register
const installEvent = new ExtendableEvent('install')
events.add(installEvent)
state.serviceWorker.state = 'installing'
await state.notify('serviceWorker')
globalThis.dispatchEvent(installEvent)
await installEvent.waitsFor()
state.serviceWorker.state = 'installed'
await state.notify('serviceWorker')
events.delete(installEvent)
return stages.install.resolve()
}
if (data?.activate?.id === state.id) {
event.stopImmediatePropagation()
await stages.install
const activateEvent = new ExtendableEvent('activate')
events.add(activateEvent)
state.serviceWorker.state = 'activating'
await state.notify('serviceWorker')
globalThis.dispatchEvent(activateEvent)
await activateEvent.waitsFor()
state.serviceWorker.state = 'activated'
await state.notify('serviceWorker')
events.delete(activateEvent)
return stages.activate.resolve()
}
if (data?.fetch?.request) {
event.stopImmediatePropagation()
await stages.activate
if (/post|put|patch|query/i.test(data.fetch.request.method)) {
const result = await ipc.request('serviceWorker.fetch.request.body', {
id: data.fetch.request.id
}, { responseType: 'arraybuffer' })
if (result.data) {
if (result.data instanceof ArrayBuffer) {
data.fetch.request.body = result.data
} else if (result.data instanceof Buffer) {
data.fetch.request.body = result.data.buffer
} else if (result.data.buffer) {
data.fetch.request.body = result.data.buffer
} else if (typeof result.data === 'object') {
data.fetch.request.body = JSON.stringify(result.data)
} else {
data.fetch.request.body = result.data
}
}
}
if (data.fetch.request.body) {
data.fetch.request.body = new Uint8Array(data.fetch.request.body)
}
const url = new URL(data.fetch.request.url)
const fetchEvent = new FetchEvent('fetch', {
clientId: data.fetch.client.id,
fetchId: data.fetch.request.id,
request: new Request(data.fetch.request.url, {
headers: new Headers(data.fetch.request.headers),
method: (data.fetch.request.method ?? 'GET').toUpperCase(),
body: data.fetch.request.body
})
})
events.add(fetchEvent)
if (protocolData) {
fetchEvent.context.data = protocolData
} else if (url.protocol !== 'socket:' && url.protocol !== 'npm') {
const result = await ipc.request('protocol.getData', {
scheme: url.protocol.replace(':', '')
})
if (result.data !== null && result.data !== undefined) {
try {
fetchEvent.context.data = JSON.parse(result.data)
} catch {
fetchEvent.context.data = result.data
}
protocolData = fetchEvent.context.data
}
}
globalThis.dispatchEvent(fetchEvent)
await fetchEvent.waitsFor()
events.delete(fetchEvent)
return
}
if (event.data?.notificationclick) {
event.stopImmediatePropagation()
globalThis.dispatchEvent(new NotificationEvent('notificationclick', {
action: event.data.notificationclick.action,
notification: new Notification(
event.data.notificationclick.title,
event.data.notificationclick.options,
event.data.notificationclick.data
)
}))
return
}
if (event.data?.notificationclose) {
event.stopImmediatePropagation()
globalThis.dispatchEvent(new NotificationEvent('notificationclose', {
action: event.data.notificationclose.action,
notification: new Notification(
event.data.notificationclose.title,
event.data.notificationclose.options,
event.data.notificationclose.data
)
}))
return
}
if (
typeof event.data?.from === 'string' &&
event.data.message &&
event.data.client
) {
event.stopImmediatePropagation()
globalThis.dispatchEvent(new ExtendableMessageEvent('message', {
source: await clients.get(event.data.client.id),
origin: event.data.client.origin,
ports: event.ports,
data: event.data.message
}))
// eslint-disable-next-line
return
}
}