Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions renderers/web_core/src/v0_9/reactivity/signals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,53 @@

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<T> {
angular: ASignal<T>;
preact: PSignal<T>;
}
interface WritableSignalKinds<T> {
angular: AWritableSignal<T>;
preact: PSignal<T>;
}
}

describe('FrameworkSignal', () => {
// Test FrameworkSignal with two sample implemenations that wrap Angular and
// Preact signals. Angular and Preact signals are good representitive samples,
// because the two common patterns - `()` vs. `.value` - are represented by
// Angular and Preact respectively.

describe('Angular variation', () => {
const AngularSignal: FrameworkSignal<ASignal<any>, AWritableSignal<any>> = {
const AngularSignal: FrameworkSignal<'angular'> = {
computed: <T>(fn: () => T) => aComputed(fn),
isSignal: (val: unknown) => isSignal(val),
wrap: <T>(val: T) => aSignal(val),
unwrap: <T>(val: ASignal<T>) => val(),
set: <T>(signal: AWritableSignal<T>, 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', () => {
Expand Down Expand Up @@ -79,12 +102,13 @@ describe('FrameworkSignal', () => {
});

describe('Preact variation', () => {
const PreactSignal: FrameworkSignal<PSignal> = {
const PreactSignal: FrameworkSignal<'preact'> = {
computed: <T>(fn: () => T) => pComputed(fn),
isSignal: (val: unknown) => val instanceof PSignal,
wrap: <T>(val: T) => new PSignal(val),
unwrap: <T>(val: PSignal<T>) => val.value,
set: <T>(signal: PSignal<T>, value: T) => (signal.value = value),
effect: (fn: () => void) => pEffect(fn),
};

it('round trip wraps and unwraps successfully', () => {
Expand Down
38 changes: 32 additions & 6 deletions renderers/web_core/src/v0_9/reactivity/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
// preact: Signal<T>;
// }
// interface WritableSignalKinds<T> {
// preact: Signal<T>;
// }
// }
//
// This <T>, while unused, is required to pass through to a given Signal impl.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface SignalKinds<T> {}

// This <T>, while unused, is required to pass through to a given Signal impl.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface WritableSignalKinds<T> {}

/**
* 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<SignalType, WriteableSignalType = SignalType> {
export interface FrameworkSignal<K extends keyof SignalKinds<any>> {
/**
* Create a computed signal for this framework.
*/
computed<T>(fn: () => T): SignalType;
computed<T>(fn: () => T): SignalKinds<T>[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<any>[K];

/**
* Wrap the value in a signal.
*/
wrap<T>(val: T): WriteableSignalType;
wrap<T>(val: T): WritableSignalKinds<T>[K];

/**
* Extract the value from a signal.
*/
unwrap<T>(val: SignalType): T;
unwrap<T>(val: SignalKinds<T>[K]): T;

/**
* Sets the value of the provided framework signal.
*/
set<T>(signal: WriteableSignalType, value: T): void;
set<T>(signal: WritableSignalKinds<T>[K], value: T): void;
}
Loading