diff --git a/package.json b/package.json index 34fb584c..208ca8cc 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,21 @@ }, "peerDependencyRules": { "allowAny": [ + "@penumbra-zone/bech32m", + "@penumbra-zone/client", + "@penumbra-zone/crypto-web", + "@penumbra-zone/getters", + "@penumbra-zone/keys", + "@penumbra-zone/perspective", + "@penumbra-zone/protobuf", + "@penumbra-zone/query", + "@penumbra-zone/services", + "@penumbra-zone/storage", + "@penumbra-zone/transport-chrome", + "@penumbra-zone/transport-dom", + "@penumbra-zone/types", + "@penumbra-zone/ui", + "@penumbra-zone/wasm", "@penumbra-zone/bech32m", "@penumbra-zone/client", "@penumbra-zone/crypto-web", diff --git a/packages/chrome-offscreen-worker/src/controller.ts b/packages/chrome-offscreen-worker/src/controller.ts index 76dc74ac..3a59a378 100644 --- a/packages/chrome-offscreen-worker/src/controller.ts +++ b/packages/chrome-offscreen-worker/src/controller.ts @@ -1,5 +1,6 @@ /// +import { OffscreenControl } from './messages/offscreen-control.js'; import { WorkerConstructorParamsPrimitive } from './messages/primitive.js'; export class OffscreenController { @@ -91,6 +92,11 @@ export class OffscreenController { const workerId = crypto.randomUUID(); + const newWorker: OffscreenControl<'new-Worker'> = { + control: 'new-Worker', + data: { workerId, init }, + }; + const { promise: workerConnection, resolve: resolveConnection, @@ -111,10 +117,7 @@ export class OffscreenController { chrome.runtime.onConnect.addListener(workerConnect); - (await session).postMessage({ - control: 'new', - data: { workerId, init }, - }); + (await session).postMessage(newWorker); void workerConnection.then(workerPort => { this.workers.set(workerId, workerPort); diff --git a/packages/chrome-offscreen-worker/src/entry.ts b/packages/chrome-offscreen-worker/src/entry.ts index d5512a42..e94befd3 100644 --- a/packages/chrome-offscreen-worker/src/entry.ts +++ b/packages/chrome-offscreen-worker/src/entry.ts @@ -1,5 +1,5 @@ import { isOffscreenControl, validOffscreenControlData } from './messages/offscreen-control'; -import { isWorkerEvent, validWorkerEventInit } from './messages/worker-event'; +import { isWorkerEvent, validWorkerEventInit, WorkerEvent } from './messages/worker-event'; import { toErrorEventInit, toMessageEventInit } from './to-init'; declare global { @@ -48,15 +48,23 @@ const constructWorker = ({ }); const caller = chrome.runtime.connect({ name: workerId }); - worker.addEventListener('error', event => - caller.postMessage({ event: 'error', init: toErrorEventInit(event) }), - ); - worker.addEventListener('messageerror', event => - caller.postMessage({ event: 'messageerror', init: toMessageEventInit(event) }), - ); - worker.addEventListener('message', event => - caller.postMessage({ event: 'message', init: toMessageEventInit(event) }), - ); + worker.addEventListener('error', event => { + console.log('-- worker output error ', event); + const json: WorkerEvent<'error'> = ['error', toErrorEventInit(event)]; + caller.postMessage(json); + }); + + worker.addEventListener('messageerror', event => { + console.log('-- worker output messageerror ', event); + const json: WorkerEvent<'messageerror'> = ['messageerror', toMessageEventInit(event)]; + caller.postMessage(json); + }); + + worker.addEventListener('message', event => { + console.log('-- worker output message ', event); + const json: WorkerEvent<'message'> = ['message', toMessageEventInit(event)]; + caller.postMessage(json); + }); // setup disconnect handler caller.onDisconnect.addListener(() => { @@ -68,9 +76,10 @@ const constructWorker = ({ caller.onMessage.addListener((json: unknown) => { console.log('entry callerInputListener', json, workerId); if (isWorkerEvent(json)) { - switch (json.event) { + const [event, init] = json; + switch (event) { case 'message': { - const { data } = validWorkerEventInit('message', json); + const { data } = validWorkerEventInit('message', init); worker.postMessage(data); return; } diff --git a/packages/chrome-offscreen-worker/src/messages/worker-event.ts b/packages/chrome-offscreen-worker/src/messages/worker-event.ts index 4e703935..1a8d8ef0 100644 --- a/packages/chrome-offscreen-worker/src/messages/worker-event.ts +++ b/packages/chrome-offscreen-worker/src/messages/worker-event.ts @@ -10,29 +10,26 @@ interface WorkerEventInitMap extends Required export type WorkerEventType = keyof WorkerEventMap; -export interface WorkerEvent { - event: T; - init: T extends WorkerEventType ? WorkerEventInitMap[T] : unknown; -} - -export const isWorkerEvent = (message: unknown): message is WorkerEvent => - typeof message === 'object' && - message != null && - 'event' in message && - typeof message.event === 'string' && - 'init' in message && - typeof message.init === 'object' && - message.init != null; +export type WorkerEvent = [ + T, + T extends WorkerEventType ? WorkerEventInitMap[T] : unknown, +]; + +export const isWorkerEvent = (message: unknown): message is WorkerEvent => { + if (Array.isArray(message) && message.length === 2) { + const [event, init] = message as [unknown, unknown]; + return typeof event === 'string' && typeof init === 'object' && init != null; + } + return false; +}; -export const hasValidWorkerEventInit = (message: { - event: string | T; - init: unknown; -}): message is WorkerEvent => { - if (typeof message.init !== 'object' || message.init == null) { +export const hasValidWorkerEventInit = ( + params: WorkerEvent, +): params is WorkerEvent => { + const [event, init] = params; + if (typeof init !== 'object' || init == null) { return false; } - - const { event, init } = message; switch (event) { case 'error': return isErrorEventInitPrimitive(init); @@ -45,11 +42,12 @@ export const hasValidWorkerEventInit = (message: { }; export const validWorkerEventInit = ( - type: T, - message: WorkerEvent, -): WorkerEvent['init'] => { - if (message.event !== type || !hasValidWorkerEventInit(message)) { + event: T, + init: unknown, +): WorkerEvent[1] => { + const message = [event, init] satisfies WorkerEvent; + if (!hasValidWorkerEventInit(message)) { throw new TypeError('invalid WorkerEvent'); } - return message.init; + return message[1]; }; diff --git a/packages/chrome-offscreen-worker/src/worker.ts b/packages/chrome-offscreen-worker/src/worker.ts index 8709c863..7677b57a 100644 --- a/packages/chrome-offscreen-worker/src/worker.ts +++ b/packages/chrome-offscreen-worker/src/worker.ts @@ -1,16 +1,8 @@ import { OffscreenController } from './controller'; import type { WorkerConstructorParamsPrimitive } from './messages/primitive'; -import { - hasValidWorkerEventInit, - isWorkerEvent, - WorkerEvent, - WorkerEventType, -} from './messages/worker-event'; +import { isWorkerEvent, validWorkerEventInit, WorkerEvent } from './messages/worker-event'; -type ErrorEventInitUnknown = Omit & { error?: unknown }; -type MessageEventInitUnknown = Omit & { data?: unknown }; - -export class OffscreenWorker implements Worker { +export class OffscreenWorker extends EventTarget implements Worker { private static control?: OffscreenController; public static configure(...params: ConstructorParameters) { @@ -19,9 +11,7 @@ export class OffscreenWorker implements Worker { private params: WorkerConstructorParamsPrimitive; - private outgoing = new EventTarget(); - private incoming = new EventTarget(); - private workerPort: Promise; + private worker: Promise; // user-assignable callback properties onerror: Worker['onerror'] = null; @@ -29,96 +19,91 @@ export class OffscreenWorker implements Worker { onmessageerror: Worker['onmessageerror'] = null; constructor(...[scriptURL, options]: ConstructorParameters) { - this.params = [ - String(scriptURL), - { - name: `${this.constructor.name} ${Date.now()} ${String(scriptURL)}`, - ...options, - }, - ]; - if (!OffscreenWorker.control) { throw new Error( - `${this.constructor.name + '.configure'} must be called before constructing ${this.constructor.name}`, + 'The static configure method must be called before constructing an instance of this class.', ); } - this.workerPort = OffscreenWorker.control.constructWorker(...this.params); - - this.outgoing.addEventListener( - 'error', - evt => void this.onerror?.call(this, evt as ErrorEvent), - ); - this.outgoing.addEventListener( - 'message', - evt => void this.onmessage?.call(this, evt as MessageEvent), - ); - this.outgoing.addEventListener( - 'messageerror', - evt => void this.onmessageerror?.call(this, evt as MessageEvent), - ); - - void this.workerPort.then( - port => { - console.log(this.params[1].name, 'got chromePort'); - port.onMessage.addListener(this.workerOutputListener); - - this.incoming.addEventListener('error', this.callerInputListener); - this.incoming.addEventListener('message', this.callerInputListener); - this.incoming.addEventListener('messageerror', this.callerInputListener); - }, - (error: unknown) => { - this.outgoing.dispatchEvent(new ErrorEvent('error', { error })); - throw new Error('Failed to attach worker port', { cause: error }); - }, - ); + console.log('offscreen worker super'); + super(); + + this.params = [ + String(scriptURL), + { name: `${this.constructor.name} ${Date.now()} ${String(scriptURL)}`, ...options }, + ]; + + console.log('calling offscreen worker construct', this.params[1].name); + this.worker = OffscreenWorker.control.constructWorker(...this.params); + + void this.worker + .then( + workerPort => { + console.log('got worker port', this.params[1].name); + workerPort.onMessage.addListener((...params) => { + console.log('activated worker output listener in background', ...params); + this.workerDispatch(...params); + }); + }, + (error: unknown) => { + this.dispatchEvent(new ErrorEvent('error', { error })); + throw new Error('Failed to attach worker port', { cause: error }); + }, + ) + .finally(() => { + console.log('worker promise settled', this.params[1].name, this.worker); + }); + + console.log('exit constructor'); } - private workerOutputListener = (json: unknown) => { - console.debug('worker workerOutputListener', json); - debugger; + private workerDispatch = (...[json]: [unknown, chrome.runtime.Port]) => { + console.debug('worker output', json); if (isWorkerEvent(json)) { - switch (json.event) { + const [event, init] = json; + switch (event) { case 'error': { - const { colno, filename, lineno, message } = validateEventInit<'error'>(json); - this.outgoing.dispatchEvent( - new ErrorEvent(json.event, { colno, filename, lineno, message }), - ); - return; - } - case 'message': { - const { data } = validateEventInit<'message'>(json); - this.outgoing.dispatchEvent(new MessageEvent(json.event, { data })); - return; + const { colno, filename, lineno, message } = validWorkerEventInit(event, init); + const dispatch = new ErrorEvent(event, { colno, filename, lineno, message }); + this.dispatchEvent(dispatch); + this.onerror?.(dispatch); + break; } + case 'message': case 'messageerror': { - const { data } = validateEventInit<'messageerror'>(json); - this.outgoing.dispatchEvent(new MessageEvent(json.event, { data })); - return; + const { data } = validWorkerEventInit(event, init); + const dispatch = new MessageEvent(event, { data }); + this.dispatchEvent(dispatch); + this[`on${event}`]?.(dispatch); + break; } default: throw new Error('Unknown event from worker', { cause: json }); - //this.outgoing.dispatchEvent(new Event(json.event, json.init)); } } }; - private callerInputListener = (evt: Event) => { - console.debug('worker callerInputListener', [evt]); + private callerDispatch = (evt: Event) => { + console.debug('worker callerInputListener', evt.type); switch (evt.type) { case 'message': { const { data } = evt as MessageEvent; - void this.workerPort.then(port => port.postMessage({ event: 'message', init: { data } })); + const workerEventMessage: WorkerEvent<'message'> = ['message', { data }]; + void this.worker.then(port => port.postMessage(workerEventMessage)); return; } - default: + case 'error': + case 'messageerror': throw new Error('Unexpected event from caller', { cause: evt }); + default: + throw new Error('Unknown event from caller', { cause: evt }); } }; terminate: Worker['terminate'] = () => { console.warn('worker terminate', this.params[1].name); - void this.workerPort.then(port => port.disconnect()); + void this.worker.then(port => port.disconnect()); + this.postMessage = () => void 0; }; postMessage: Worker['postMessage'] = (...args) => { @@ -133,35 +118,6 @@ export class OffscreenWorker implements Worker { const messageEvent = new MessageEvent('message', { data }); - this.incoming.dispatchEvent(messageEvent); - }; - - dispatchEvent: Worker['dispatchEvent'] = event => { - console.debug('worker dispatchEvent', this.params[1].name, [event]); - return this.incoming.dispatchEvent(event); - }; - - addEventListener: Worker['addEventListener'] = ( - ...args: Parameters - ) => { - console.debug('worker addEventListener', this.params[1].name, args); - this.outgoing.addEventListener(...args); - }; - - removeEventListener: Worker['removeEventListener'] = ( - ...args: Parameters - ) => { - console.debug('worker removeEventListener', this.params[1].name, args); - this.outgoing.removeEventListener(...args); + this.callerDispatch(messageEvent); }; } - -const validateEventInit = (message: { - event: T | string; - init: NonNullable; -}): WorkerEvent['init'] => { - if (!hasValidWorkerEventInit(message)) { - throw new TypeError('Invalid event init', { cause: message }); - } - return message.init; -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 231271e3..9bb1df88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2092,7 +2092,7 @@ packages: '@penumbra-zone/types': file:///Users/yet/Developer/github.com/penumbra-zone/web/packages/types/penumbra-zone-types-22.0.0.tgz '@penumbra-zone/getters@file:../../penumbra-zone/web/packages/getters/penumbra-zone-getters-16.0.0.tgz': - resolution: {integrity: sha512-XElx8WQQOc88AtDqMus/jQ4WE69MZtDAhZ47iCdH84/eAt3Uf5ZeGUGoLspBRVgNDZm//FQesKyibwout2V/Og==, tarball: file:../../penumbra-zone/web/packages/getters/penumbra-zone-getters-16.0.0.tgz} + resolution: {integrity: sha512-InvIstvwV3TtZX5qF59NajV8htwmtFeN1kVc4UNVj5s6TqhN65QJmnGodVDyqJb050dXQsJQjJwp0/2/VFUjPw==, tarball: file:../../penumbra-zone/web/packages/getters/penumbra-zone-getters-16.0.0.tgz} version: 16.0.0 peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -2105,7 +2105,7 @@ packages: hasBin: true '@penumbra-zone/perspective@file:../../penumbra-zone/web/packages/perspective/penumbra-zone-perspective-29.0.0.tgz': - resolution: {integrity: sha512-NZ1PDYtAp5BA2G8yDa9uLLkx2mg99BhUwrohQQ8kX51s2gnFbH1zujzRYnojzdc+DqPD3j07KkMPeDivUIyn5w==, tarball: file:../../penumbra-zone/web/packages/perspective/penumbra-zone-perspective-29.0.0.tgz} + resolution: {integrity: sha512-UPzD5DxONEcgqolNyFRXJjIPMjq9CMB/TOaCXjF2tJSNwxnXd+N7AVYaNBHkl4XrWBXGPhlKTEa8BU8s2YC6uA==, tarball: file:../../penumbra-zone/web/packages/perspective/penumbra-zone-perspective-29.0.0.tgz} version: 29.0.0 peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -2121,7 +2121,7 @@ packages: '@bufbuild/protobuf': ^1.10.0 '@penumbra-zone/query@file:../../penumbra-zone/web/packages/query/penumbra-zone-query-30.0.0.tgz': - resolution: {integrity: sha512-2I+0KjyTq8jJqMvpfb5FxcHv6aaQlMW1q4vVjkN8m4GTgJCl7LcTrbZtR1p15tgY+nJehT05MrYIt62MOjIIEg==, tarball: file:../../penumbra-zone/web/packages/query/penumbra-zone-query-30.0.0.tgz} + resolution: {integrity: sha512-C+0rCITbTD7dUmwacpl+msOT8+KJeIWMXT8CA8XEz/8kH/N2Ku+g264yzHZ14SiSGfRhTjf5BFSeXbqMWD08ew==, tarball: file:../../penumbra-zone/web/packages/query/penumbra-zone-query-30.0.0.tgz} version: 30.0.0 peerDependencies: '@penumbra-zone/bech32m': file:///Users/yet/Developer/github.com/penumbra-zone/web/packages/bech32m/penumbra-zone-bech32m-7.0.0.tgz @@ -2132,7 +2132,7 @@ packages: '@penumbra-zone/wasm': file:///Users/yet/Developer/github.com/penumbra-zone/web/packages/wasm/penumbra-zone-wasm-27.0.0.tgz '@penumbra-zone/services@file:../../penumbra-zone/web/packages/services/penumbra-zone-services-33.0.0.tgz': - resolution: {integrity: sha512-RHvF8WMdKCtEyoWAvE8iuv++p5ZQgkSA6FgigNPL9Rth47feNqZXGCVlcB3XOyncIJ6omLFSJ6GkSayM4/Rk3A==, tarball: file:../../penumbra-zone/web/packages/services/penumbra-zone-services-33.0.0.tgz} + resolution: {integrity: sha512-i58k0F9LRTIAQGPIu9Dq7u4/meXRKt5as3sUI8pEY84Y5wDAED80dzP6nluxz/Pludgg8ascvpNhv38IGkPXOw==, tarball: file:../../penumbra-zone/web/packages/services/penumbra-zone-services-33.0.0.tgz} version: 33.0.0 peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -2149,7 +2149,7 @@ packages: '@penumbra-zone/wasm': file:///Users/yet/Developer/github.com/penumbra-zone/web/packages/wasm/penumbra-zone-wasm-27.0.0.tgz '@penumbra-zone/storage@file:../../penumbra-zone/web/packages/storage/penumbra-zone-storage-29.0.0.tgz': - resolution: {integrity: sha512-h8SZLC8ftvZmeCRo2J4ym1oAiJM2Mo0G7tD7zr/LU1pFNSWzcUlBRyukJ9582YwBn3xlqzxycdC5DCOYT+lMjw==, tarball: file:../../penumbra-zone/web/packages/storage/penumbra-zone-storage-29.0.0.tgz} + resolution: {integrity: sha512-XKSG/2+1+whdfyHXz4663ha6lM4jTxObfCwNEXMT/nntEpX86QcxvUkC/mAJvlMKvpMea98IlEKOMDUT7jR4aA==, tarball: file:../../penumbra-zone/web/packages/storage/penumbra-zone-storage-29.0.0.tgz} version: 29.0.0 peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -2173,7 +2173,7 @@ packages: version: 7.5.0 '@penumbra-zone/types@file:../../penumbra-zone/web/packages/types/penumbra-zone-types-22.0.0.tgz': - resolution: {integrity: sha512-1ZbfGr+/2SQ6XipdEo6AADt0z2+KlCMqD3sonTzWynzqRxafhrRU8jPK3e9lCguL6sSn2PvIkmnKA9pvGXSV0Q==, tarball: file:../../penumbra-zone/web/packages/types/penumbra-zone-types-22.0.0.tgz} + resolution: {integrity: sha512-RjFabWzEDITYiBoqe8qUy46zQUIAUPxJFGaDI744BM+vCWkcXaniOwZ4K5WCrmjuojXnreEgtiqUnnTpjT2NOQ==, tarball: file:../../penumbra-zone/web/packages/types/penumbra-zone-types-22.0.0.tgz} version: 22.0.0 peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -2182,7 +2182,7 @@ packages: '@penumbra-zone/protobuf': file:///Users/yet/Developer/github.com/penumbra-zone/web/packages/protobuf/penumbra-zone-protobuf-6.0.0.tgz '@penumbra-zone/wasm@file:../../penumbra-zone/web/packages/wasm/penumbra-zone-wasm-27.0.0.tgz': - resolution: {integrity: sha512-+qJ9dg5lj9RWS4tsUS64BdW4Xy2zozZ5pMNoHQLuea1ZTf0sahGcCmOEmac9elysX6d0Dc5AwoDor5wgQpKMxg==, tarball: file:../../penumbra-zone/web/packages/wasm/penumbra-zone-wasm-27.0.0.tgz} + resolution: {integrity: sha512-WcLzm4OnTU5WeOuYArgsIhvHIE7Ertr6ojkGWE2Y6SAc2rjlf+H7AZ8rHH1GuRbp4uTp2awr29GMQDwAwtsIGQ==, tarball: file:../../penumbra-zone/web/packages/wasm/penumbra-zone-wasm-27.0.0.tgz} version: 27.0.0 peerDependencies: '@bufbuild/protobuf': ^1.10.0