diff --git a/renderers/web_core/CHANGELOG.md b/renderers/web_core/CHANGELOG.md index e37e86ced..5863dbb58 100644 --- a/renderers/web_core/CHANGELOG.md +++ b/renderers/web_core/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.9.1 + +- Add new `FrameworkSignal` concept, which represents a generic signal from a + given framework like Preact or Angular. + - Unused in this version; future versions will introduce this throughout web + core and will likely be breaking changes. + ## 0.8.8 - Add the ability to access the `schema` of a component in a type-safe way. diff --git a/renderers/web_core/package.json b/renderers/web_core/package.json index ba3cb73ea..819038f7b 100644 --- a/renderers/web_core/package.json +++ b/renderers/web_core/package.json @@ -1,6 +1,6 @@ { "name": "@a2ui/web_core", - "version": "0.9.0", + "version": "0.9.1", "description": "A2UI Core Library", "homepage": "https://a2ui.org/", "repository": { diff --git a/renderers/web_core/src/v0_9/reactivity/signals.test.ts b/renderers/web_core/src/v0_9/reactivity/signals.test.ts index 6f02f4b17..31bd52934 100644 --- a/renderers/web_core/src/v0_9/reactivity/signals.test.ts +++ b/renderers/web_core/src/v0_9/reactivity/signals.test.ts @@ -16,17 +16,33 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import {Signal as PSignal, computed as pComputed} from '@preact/signals-core'; +import { + Signal as PSignal, + computed as pComputed, + effect as pEffect, +} from '@preact/signals-core'; import { signal as aSignal, computed as aComputed, Signal as ASignal, WritableSignal as AWritableSignal, isSignal, + effect as aEffect, } from '@angular/core'; import {FrameworkSignal} from './signals'; +declare module './signals' { + interface SignalKinds { + angular: ASignal; + preact: PSignal; + } + interface WritableSignalKinds { + angular: AWritableSignal; + preact: PSignal; + } +} + describe('FrameworkSignal', () => { // Test FrameworkSignal with two sample implemenations that wrap Angular and // Preact signals. Angular and Preact signals are good representitive samples, @@ -34,12 +50,19 @@ describe('FrameworkSignal', () => { // Angular and Preact respectively. describe('Angular variation', () => { - const AngularSignal: FrameworkSignal, AWritableSignal> = { + const AngularSignal: FrameworkSignal<'angular'> = { computed: (fn: () => T) => aComputed(fn), isSignal: (val: unknown) => isSignal(val), wrap: (val: T) => aSignal(val), unwrap: (val: ASignal) => val(), set: (signal: AWritableSignal, value: T) => signal.set(value), + effect: (fn: () => void, cleanupCallback: () => void) => { + const e = aEffect(cleanupRegisterFn => { + cleanupRegisterFn(cleanupCallback); + fn(); + }); + return () => e.destroy(); + }, }; it('round trip wraps and unwraps successfully', () => { @@ -79,12 +102,13 @@ describe('FrameworkSignal', () => { }); describe('Preact variation', () => { - const PreactSignal: FrameworkSignal = { + const PreactSignal: FrameworkSignal<'preact'> = { computed: (fn: () => T) => pComputed(fn), isSignal: (val: unknown) => val instanceof PSignal, wrap: (val: T) => new PSignal(val), unwrap: (val: PSignal) => val.value, set: (signal: PSignal, value: T) => (signal.value = value), + effect: (fn: () => void) => pEffect(fn), }; it('round trip wraps and unwraps successfully', () => { diff --git a/renderers/web_core/src/v0_9/reactivity/signals.ts b/renderers/web_core/src/v0_9/reactivity/signals.ts index c636a5066..3c31578e2 100644 --- a/renderers/web_core/src/v0_9/reactivity/signals.ts +++ b/renderers/web_core/src/v0_9/reactivity/signals.ts @@ -14,34 +14,60 @@ * limitations under the License. */ +// SignalKinds and WritableSignalKinds are declared in such a way that +// downstream library impls can dynamically provide their Signal implementations +// in a type-safe way. Usage downstream might look something like: +// +// declare module '../reactivity/signals' { +// interface SignalKinds { +// preact: Signal; +// } +// interface WritableSignalKinds { +// preact: Signal; +// } +// } +// +// This , while unused, is required to pass through to a given Signal impl. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface SignalKinds {} + +// This , while unused, is required to pass through to a given Signal impl. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface WritableSignalKinds {} + /** * A generic representation of a Signal that could come from any framework. * For any library building on top of A2UI's web core lib, this must be * implemented for their associated signals implementation. */ -export interface FrameworkSignal { +export interface FrameworkSignal> { /** * Create a computed signal for this framework. */ - computed(fn: () => T): SignalType; + computed(fn: () => T): SignalKinds[K]; + + /** + * Run a reactive effect. + */ + effect(fn: () => void, cleanupCallback?: () => void): () => void; /** * Check if an arbitrary object is a framework signal. */ - isSignal(val: unknown): val is SignalType; + isSignal(val: unknown): val is SignalKinds[K]; /** * Wrap the value in a signal. */ - wrap(val: T): WriteableSignalType; + wrap(val: T): WritableSignalKinds[K]; /** * Extract the value from a signal. */ - unwrap(val: SignalType): T; + unwrap(val: SignalKinds[K]): T; /** * Sets the value of the provided framework signal. */ - set(signal: WriteableSignalType, value: T): void; + set(signal: WritableSignalKinds[K], value: T): void; }