From 4d054adbca8475ec60648472d0ea3af7a792d686 Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Sat, 17 Aug 2024 22:38:57 -0400 Subject: [PATCH] Scaffold the volatile source type This introduces a new `Signal.Volatile` function. It follows the initial proposal here: https://github.com/tc39/proposal-signals/issues/237 Volatile sources bring outside data into the computation graph. --- src/volatile.ts | 32 ++++++++++++++++++++++++++++++++ src/wrapper.ts | 30 +++++++++++++++++++++++++++++- tests/Signal/volatile.test.ts | 10 ++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/volatile.ts create mode 100644 tests/Signal/volatile.test.ts diff --git a/src/volatile.ts b/src/volatile.ts new file mode 100644 index 0000000..f0b63de --- /dev/null +++ b/src/volatile.ts @@ -0,0 +1,32 @@ +import {REACTIVE_NODE, type ReactiveNode, producerAccessed} from './graph'; + +/** + * Volatile functions read from external sources. They can change at any time + * without notifying the graph. If the source supports it, optionally we can + * subscribe to changes while observed. + * + * Unless the external source is actively being observed, we have to assume + * it's stale and bust the cache of everything downstream. + */ +export function createVolatile(getSnapshot: () => T): VolatileNode { + const node: VolatileNode = Object.create(REACTIVE_NODE); + node.getSnapshot = getSnapshot; + + return node; +} + +export function volatileGetFn(this: VolatileNode): T { + producerAccessed(this); + + // TODO: + // - Cache when live. + // - Handle errors in live snapshots. + // - Throw if dependencies are used in the snapshot. + // - Bust downstream caches when not live. + return this.getSnapshot(); +} + +export interface VolatileNode extends ReactiveNode { + /** Read state from the outside world. May be expensive. */ + getSnapshot: () => T; +} diff --git a/src/wrapper.ts b/src/wrapper.ts index 1d3f165..b2201dc 100644 --- a/src/wrapper.ts +++ b/src/wrapper.ts @@ -29,10 +29,14 @@ import { producerRemoveLiveConsumerAtIndex, } from './graph.js'; import {createSignal, signalGetFn, signalSetFn, type SignalNode} from './signal.js'; +import {createVolatile, volatileGetFn, type VolatileNode} from './volatile'; const NODE: unique symbol = Symbol('node'); -let isState: (s: any) => boolean, isComputed: (s: any) => boolean, isWatcher: (s: any) => boolean; +let isState: (s: any) => boolean, + isVolatile: (s: any) => boolean, + isComputed: (s: any) => boolean, + isWatcher: (s: any) => boolean; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Signal { @@ -110,6 +114,30 @@ export namespace Signal { } } + export class Volatile { + readonly [NODE]: VolatileNode; + + #brand() {} + + static { + isVolatile = (v: any): v is Volatile => #brand in v; + } + + constructor(getSnapshot: () => T, _options?: Signal.Options) { + const node = createVolatile(getSnapshot); + this[NODE] = node; + + // TODO: Implement subscribe. + } + + get(): T { + if (!isVolatile(this)) + throw new TypeError('Wrong receiver type for Signal.Volatile.prototype.get'); + + return (volatileGetFn).call(this[NODE]); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnySignal = State | Computed; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/tests/Signal/volatile.test.ts b/tests/Signal/volatile.test.ts new file mode 100644 index 0000000..35aa0d4 --- /dev/null +++ b/tests/Signal/volatile.test.ts @@ -0,0 +1,10 @@ +import {describe, expect, it} from 'vitest'; +import {Signal} from '../../src/wrapper.js'; + +describe('Signal.Volatile', () => { + it('reads the value using the given function', () => { + const volatile = new Signal.Volatile(() => 'value'); + + expect(volatile.get()).toBe('value'); + }); +});