Skip to content

Commit

Permalink
Scaffold the volatile source type
Browse files Browse the repository at this point in the history
This introduces a new `Signal.Volatile` function. It follows the initial
proposal here: tc39/proposal-signals#237

Volatile sources bring outside data into the computation graph.
  • Loading branch information
PsychoLlama committed Aug 18, 2024
1 parent e8f5a43 commit 4d054ad
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 1 deletion.
32 changes: 32 additions & 0 deletions src/volatile.ts
Original file line number Diff line number Diff line change
@@ -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<T>(getSnapshot: () => T): VolatileNode<T> {
const node: VolatileNode<T> = Object.create(REACTIVE_NODE);
node.getSnapshot = getSnapshot;

return node;
}

export function volatileGetFn<T>(this: VolatileNode<T>): 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<T> extends ReactiveNode {
/** Read state from the outside world. May be expensive. */
getSnapshot: () => T;
}
30 changes: 29 additions & 1 deletion src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -110,6 +114,30 @@ export namespace Signal {
}
}

export class Volatile<T> {
readonly [NODE]: VolatileNode<T>;

#brand() {}

static {
isVolatile = (v: any): v is Volatile<any> => #brand in v;
}

constructor(getSnapshot: () => T, _options?: Signal.Options<T>) {
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<T>).call(this[NODE]);
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySignal<T = any> = State<T> | Computed<T>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
10 changes: 10 additions & 0 deletions tests/Signal/volatile.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit 4d054ad

Please sign in to comment.