diff --git a/.vscode/settings.json b/.vscode/settings.json index f2f9457..fe7af1a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "ents", "plah", + "Shae", "testresult", "transferables" ] diff --git a/packages/shadow-ents-e2e/pages/shae-worker.html b/packages/shadow-ents-e2e/pages/shae-worker.html new file mode 100644 index 0000000..b00bed3 --- /dev/null +++ b/packages/shadow-ents-e2e/pages/shae-worker.html @@ -0,0 +1,14 @@ + + + + + + + shae-worker + + + +
+ + + diff --git a/packages/shadow-ents-e2e/src/remote-worker-env.js b/packages/shadow-ents-e2e/src/remote-worker-env.js deleted file mode 100644 index 2b97b5a..0000000 --- a/packages/shadow-ents-e2e/src/remote-worker-env.js +++ /dev/null @@ -1,44 +0,0 @@ -import {ComponentContext, RemoteWorkerEnv, ShadowEnv, ViewComponent} from '@spearwolf/shadow-ents'; -import './style.css'; -import {testAsyncAction} from './testAsyncAction.js'; -import {testBooleanAction} from './testBooleanAction.js'; - -main(); - -async function main() { - const shadowEnv = new ShadowEnv(); - - shadowEnv.view = ComponentContext.get(); - shadowEnv.envProxy = new RemoteWorkerEnv(); - - window.shadowEnv = shadowEnv; - console.log('shadowEnv', shadowEnv); - - await testAsyncAction('shadow-env-ready', async () => { - await shadowEnv.ready(); - }); - - await testAsyncAction('shadow-env-importScript', async () => { - await shadowEnv.envProxy.importScript('/mod-hello.js'); - }); - - testBooleanAction('shadow-env-isReady', shadowEnv.isReady); - - const foo = new ViewComponent('foo'); - foo.setProperty('xyz', 123); - - foo.on('helloFromFoo', (...args) => { - console.log('HELLO', ...args); - }); - - const bar = new ViewComponent('bar', {parent: foo}); - bar.setProperty('plah', 666); - - await testAsyncAction('shadow-env-1st-sync', async () => { - await shadowEnv.sync(); - }); - - await testAsyncAction('shadow-env-hello', async () => { - await foo.onceAsync('helloFromFoo'); - }); -} diff --git a/packages/shadow-ents-e2e/src/shae-worker.js b/packages/shadow-ents-e2e/src/shae-worker.js new file mode 100644 index 0000000..a26e9d2 --- /dev/null +++ b/packages/shadow-ents-e2e/src/shae-worker.js @@ -0,0 +1,26 @@ +import {GlobalNS} from '@spearwolf/shadow-ents'; +import '@spearwolf/shadow-ents/shae-worker.js'; +import './style.css'; +import {testAsyncAction} from './testAsyncAction.js'; +import {testBooleanAction} from './testBooleanAction.js'; + +main(); + +async function main() { + const worker = document.getElementById('worker'); + + window.worker = worker; + console.log('shae-worker element', worker); + + await testAsyncAction('shae-worker-whenDefined', () => customElements.whenDefined('shae-worker')); + + const shadowEnv = worker.shadowEnv; + window.shadowEnv = shadowEnv; + console.log('shadowEnv', shadowEnv); + + testBooleanAction('shae-worker-ns', () => worker.ns === GlobalNS); + + await testAsyncAction('shadow-env-ready', async () => { + await shadowEnv.ready(); + }); +} diff --git a/packages/shadow-ents-e2e/src/style.css b/packages/shadow-ents-e2e/src/style.css index 96409f1..1b2bd60 100644 --- a/packages/shadow-ents-e2e/src/style.css +++ b/packages/shadow-ents-e2e/src/style.css @@ -56,6 +56,7 @@ main { } #tests { + margin-top: 1rem; margin-left: auto; margin-right: auto; display: grid; diff --git a/packages/shadow-ents/package.json b/packages/shadow-ents/package.json index bcff4db..dc2c44a 100644 --- a/packages/shadow-ents/package.json +++ b/packages/shadow-ents/package.json @@ -49,6 +49,10 @@ "default": "./dist/src/shadow-local-env.js", "types": "./dist/src/shadow-local-env.d.ts" }, + "./shae-worker.js": { + "default": "./dist/src/shae-worker.js", + "types": "./dist/src/shae-worker.d.ts" + }, "./shadow-worker.js": { "default": "./dist/src/shadow-worker.js", "types": "./dist/src/shadow-worker.d.ts" @@ -56,6 +60,7 @@ }, "sideEffects": [ "build/src/view/ComponentContext.js", + "build/src/shae-worker.js", "build/src/shadow-worker.js", "build/src/shadow-entity.js", "build/src/shadow-local-env.js", @@ -65,6 +70,7 @@ "build/src/core.js", "build/src/index.js", "dist/src/view/ComponentContext.js", + "dist/src/shae-worker.js", "dist/src/shadow-worker.js", "dist/src/shadow-entity.js", "dist/src/shadow-local-env.js", diff --git a/packages/shadow-ents/package.override.json b/packages/shadow-ents/package.override.json index de4d3d9..47d46db 100644 --- a/packages/shadow-ents/package.override.json +++ b/packages/shadow-ents/package.override.json @@ -6,6 +6,7 @@ "src/core.js", "src/index.js", "src/view/ComponentContext.js", + "src/shae-worker.js", "src/shadow-worker.js", "src/shadow-entity.js", "src/shadow-local-env.js", diff --git a/packages/shadow-ents/src/bundle.ts b/packages/shadow-ents/src/bundle.ts index c7017af..e9b11ff 100644 --- a/packages/shadow-ents/src/bundle.ts +++ b/packages/shadow-ents/src/bundle.ts @@ -1,8 +1,9 @@ import './shadow-entity.js'; -import './shadow-env.js'; import './shadow-env-legacy.js'; +import './shadow-env.js'; import './shadow-local-env.js'; import './shadow-worker.js'; +import './shae-worker.js'; declare global { // eslint-disable-next-line no-var diff --git a/packages/shadow-ents/src/elements/ShadowEntityElement.ts b/packages/shadow-ents/src/elements/ShadowEntityElement.ts index 337b622..a172492 100644 --- a/packages/shadow-ents/src/elements/ShadowEntityElement.ts +++ b/packages/shadow-ents/src/elements/ShadowEntityElement.ts @@ -51,7 +51,7 @@ export class ShadowEntityElement extends HTMLElement { this.getContextByType$$(ShadowElementType.ShadowEnv)!.get((env) => { this.shadowEnvElement = env as unknown as IShadowEnvElementLegacy; - // this.componentContext = (env && (env as unknown as IShadowEnvElementLegacy).getComponentContext()) || undefined; + this.componentContext = (env && (env as unknown as IShadowEnvElementLegacy).getComponentContext()) || undefined; }); this.parentEntity$((parent) => this.#onParentEntityChanged(parent)); @@ -315,7 +315,6 @@ export class ShadowEntityElement extends HTMLElement { } #changeNamespace = () => { - this.componentContext = ComponentContext.get(this.ns || GlobalNS); // TODO a namespace change should trigger a re-connection of all descendants if (this.isConnected) { this.#reconnectToShadowTree(); diff --git a/packages/shadow-ents/src/elements/ShaeEntElement.ts b/packages/shadow-ents/src/elements/ShaeEntElement.ts new file mode 100644 index 0000000..21eaf09 --- /dev/null +++ b/packages/shadow-ents/src/elements/ShaeEntElement.ts @@ -0,0 +1,64 @@ +import {createSignal} from '@spearwolf/signalize'; +import {GlobalNS} from '../constants.js'; +import {ComponentContext, ViewComponent} from '../core.js'; +import {generateUUID} from '../generateUUID.js'; +import {toNamespace} from '../toNamespace.js'; + +export class ShaeEntElement extends HTMLElement { + static observedAttributes = ['ns', 'token']; + + readonly isShaeElement = true; + readonly isShaeEntElement = true; + + readonly uuid = generateUUID(); + + readonly #namespace = createSignal(); + readonly #componentContext = createSignal(); + readonly #viewComponent = createSignal(); + + get ns() { + return this.#namespace.value; + } + + set ns(ns: string | symbol) { + this.#namespace.set(toNamespace(ns)); + } + + constructor() { + super(); + + this.#namespace.onChange((ns) => { + this.#componentContext.set(ComponentContext.get(ns)); + + if (typeof ns === 'symbol') { + if (this.hasAttribute('ns')) { + this.removeAttribute('ns'); + } + } else { + this.setAttribute('ns', ns); + } + }); + + this.#componentContext.onChange((context) => { + const vc = this.#viewComponent.value; + + if (context == null) { + if (vc != null) { + vc.destroy(); + } + this.#viewComponent.set(undefined); + return; + } + + if (vc == null) { + this.#viewComponent.set(new ViewComponent(this.uuid, {context})); + } else { + vc.context = context; + } + }); + + this.#namespace.set(GlobalNS); + } + + connectedCallback() {} +} diff --git a/packages/shadow-ents/src/elements/ShaeWorkerElement.ts b/packages/shadow-ents/src/elements/ShaeWorkerElement.ts new file mode 100644 index 0000000..65319bd --- /dev/null +++ b/packages/shadow-ents/src/elements/ShaeWorkerElement.ts @@ -0,0 +1,128 @@ +import {createSignal} from '@spearwolf/signalize'; +import {GlobalNS} from '../constants.js'; +import {toNamespace} from '../toNamespace.js'; +import {ComponentContext} from '../view/ComponentContext.js'; +import {LocalShadowObjectEnv} from '../view/LocalShadowObjectEnv.js'; +import {RemoteWorkerEnv} from '../view/RemoteWorkerEnv.js'; +import {ShadowEnv} from '../view/ShadowEnv.js'; + +const readNamespaceAttribute = (el: HTMLElement) => toNamespace(el.getAttribute('ns')); + +const readBooleanAttribute = (el: HTMLElement, name: string) => { + if (el.hasAttribute(name)) { + const val = el.getAttribute(name)?.trim()?.toLowerCase() || 'on'; + return ['true', 'on', 'yes', 'local'].includes(val); + } + return false; +}; + +const AttrNamespace = 'ns'; +const AttrLocal = 'local'; + +export class ShaeWorkerElement extends HTMLElement { + static observedAttributes = [AttrNamespace]; + + readonly isShaeElement = true; + readonly isShaeWorkerElement = true; + + readonly shadowEnv = new ShadowEnv(); + + readonly #ns = createSignal(GlobalNS); + + #shouldDestroy = false; + + constructor() { + super(); + + this.#ns.onChange((ns) => { + this.shadowEnv.view = ComponentContext.get(ns); + }); + + this.shadowEnv.on(ShadowEnv.ContextCreated, () => { + this.dispatchEvent( + new CustomEvent(ShadowEnv.ContextCreated.toLowerCase(), { + bubbles: false, + detail: {shadowEnv: this.shadowEnv}, + }), + ); + }); + + this.shadowEnv.on(ShadowEnv.ContextLost, () => { + this.dispatchEvent( + new CustomEvent(ShadowEnv.ContextLost.toLowerCase(), { + bubbles: false, + detail: {shadowEnv: this.shadowEnv}, + }), + ); + }); + + this.shadowEnv.on(ShadowEnv.AfterSync, () => { + this.dispatchEvent( + new CustomEvent(ShadowEnv.AfterSync.toLowerCase(), { + bubbles: false, + detail: {shadowEnv: this.shadowEnv}, + }), + ); + }); + } + + get ns(): string | symbol { + return this.#ns.value; + } + + set ns(ns: string | symbol) { + if (typeof ns === 'symbol') { + this.#ns.set(ns); + } else { + this.#ns.set(toNamespace(ns)); + } + } + + connectedCallback() { + this.start(); + } + + disconnectedCallback() { + this.#deferDestroy(); + } + + attributeChangedCallback(name: string) { + if (name === AttrNamespace) { + this.#ns.set(readNamespaceAttribute(this)); + } + if (name === AttrLocal) { + if (this.shadowEnv.envProxy != null) { + throw new Error( + '[ShaeWorkerElement] Changing the "local" attribute after the shadowEnv has been created is not supported.', + ); + } + } + } + + start(): Promise { + this.#shouldDestroy = false; + if (this.shadowEnv.view == null) { + this.shadowEnv.view = ComponentContext.get(this.#ns.value); + } + if (this.shadowEnv.envProxy == null) { + const envProxy = readBooleanAttribute(this, AttrLocal) ? new LocalShadowObjectEnv() : new RemoteWorkerEnv(); + this.shadowEnv.envProxy = envProxy; + } + return this.shadowEnv.ready(); + } + + destroy() { + this.shadowEnv.envProxy = undefined; + } + + #deferDestroy() { + if (!this.#shouldDestroy) { + this.#shouldDestroy = true; + queueMicrotask(() => { + if (this.#shouldDestroy) { + this.destroy(); + } + }); + } + } +} diff --git a/packages/shadow-ents/src/elements/constants.ts b/packages/shadow-ents/src/elements/constants.ts index 79a8825..f8511e3 100644 --- a/packages/shadow-ents/src/elements/constants.ts +++ b/packages/shadow-ents/src/elements/constants.ts @@ -10,5 +10,7 @@ export const SHADOW_ELEMENT_ENTITY = 'shadow-entity'; export const SHADOW_ELEMENT_ENV = 'shadow-env'; export const SHADOW_ELEMENT_ENV_LEGACY = 'shadow-env-legacy'; export const SHADOW_ELEMENT_LOCAL_ENV = 'shadow-local-env'; - export const SHADOW_ELEMENT_WORKER = 'shadow-worker'; + +export const SHAE_WORKER = 'shae-worker'; +export const SHAE_ENT = 'shae-ent'; diff --git a/packages/shadow-ents/src/index.ts b/packages/shadow-ents/src/index.ts index 4444c91..31b4df3 100644 --- a/packages/shadow-ents/src/index.ts +++ b/packages/shadow-ents/src/index.ts @@ -4,6 +4,8 @@ export * from './elements/ShadowEntityElement.js'; export * from './elements/ShadowEnvElement.js'; export * from './elements/ShadowEnvElementLegacy.js'; export * from './elements/ShadowLocalEnvElement.js'; +export * from './elements/ShaeEntElement.js'; +export * from './elements/ShaeWorkerElement.js'; export * from './elements/constants.js'; export * from './elements/isShadowElement.js'; export * from './entities/Kernel.js'; diff --git a/packages/shadow-ents/src/shae-worker.ts b/packages/shadow-ents/src/shae-worker.ts new file mode 100644 index 0000000..b864679 --- /dev/null +++ b/packages/shadow-ents/src/shae-worker.ts @@ -0,0 +1,4 @@ +import {ShaeWorkerElement} from './elements/ShaeWorkerElement.js'; +import {SHAE_WORKER} from './elements/constants.js'; + +customElements.define(SHAE_WORKER, ShaeWorkerElement); diff --git a/packages/shadow-ents/src/view/ShadowEnv.ts b/packages/shadow-ents/src/view/ShadowEnv.ts index 3838ec4..1acd8b6 100644 --- a/packages/shadow-ents/src/view/ShadowEnv.ts +++ b/packages/shadow-ents/src/view/ShadowEnv.ts @@ -1,8 +1,8 @@ import {eventize, type EventizeApi} from '@spearwolf/eventize'; import {createEffect, type SignalReader} from '@spearwolf/signalize'; import {signal, signalReader} from '@spearwolf/signalize/decorators'; -import type {MessageToViewEvent} from '../core.js'; -import type {ComponentContext} from './ComponentContext.js'; +import {type MessageToViewEvent} from '../core.js'; +import {ComponentContext} from './ComponentContext.js'; import type {IShadowObjectEnvProxy} from './IShadowObjectEnvProxy.js'; export interface ShadowEnv extends EventizeApi {} @@ -134,4 +134,6 @@ export class ShadowEnv { console.log('ShadowEnv: onMessageToView', event.type, event.data); this.view?.dispatchMessage(event.uuid, event.type, event.data); } + + // TODO ShadowEnv#destroy() }