Skip to content

Commit

Permalink
feat: ReadonlyState is now only an interfac
Browse files Browse the repository at this point in the history
  • Loading branch information
MKRhere committed Nov 12, 2023
1 parent 90c89ad commit b2982d4
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 38 deletions.
3 changes: 1 addition & 2 deletions hyper/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Tag } from "./lib/tags.ts";
import { EmptyElements } from "./lib/emptyElements.ts";
import { Attr } from "./lib/attributes.ts";
import { Falsy, isFalsy } from "./util.ts";
import { State, ReadonlyState } from "./state.ts";
import { State, type ReadonlyState } from "./state.ts";

export type NonEmptyElement = Exclude<Tag, EmptyElements>;

Expand Down Expand Up @@ -46,7 +46,6 @@ export type HyperNodeish<T extends Tag = Tag> =
| HyperTextNode
| HyperHTMLStringNode
| Falsy
| State<HyperNode<T> | HyperTextNode | HyperHTMLStringNode | Falsy>
| ReadonlyState<HyperNode<T> | HyperTextNode | HyperHTMLStringNode | Falsy>;

// deno-lint-ignore no-explicit-any
Expand Down
2 changes: 1 addition & 1 deletion hyper/render/dom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { guessEnv } from "../guessEnv.ts";
import { Falsy, isFalsy } from "../util.ts";
import { State, ReadonlyState } from "../state.ts";
import { State, type ReadonlyState } from "../state.ts";
import { Document, HTMLElement, Node, Text } from "../lib/dom.ts";
import { HyperHTMLStringNode, HyperNodeish } from "../node.ts";

Expand Down
79 changes: 44 additions & 35 deletions hyper/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ type MergedStateValue<Obj extends Record<string, State>> = {
[key in keyof Obj]: [key: key, value: Obj[key]["value"]];
}[keyof Obj];

export class ReadonlyState<T = any> {
protected subscribers: Subscriber<T>[] = [];
export interface ReadonlyState<T = any> {
value: T;
listen(listener: Subscriber<T>): void;
map<U>(mapper: (t: T) => U): ReadonlyState<U>;
into(state: State<T>): void;
}

const StateSymbol = Symbol("@hyperactive/state");

export class State<T = any> implements ReadonlyState<T> {
#subscribers: Subscriber<T>[] = [];
#state: { value: T };
[StateSymbol] = true;

constructor(state: { value: T }) {
constructor(value: T) {
const state = { value };
this.#state = state;
}

Expand All @@ -17,33 +28,7 @@ export class ReadonlyState<T = any> {
}

static isState<X>(x: X): x is Extract<X, State | ReadonlyState> {
return x instanceof ReadonlyState;
}

listen(listener: Subscriber<T>) {
this.subscribers.push(listener);
}

map<U>(mapper: (t: T) => U) {
const s = new State(mapper(this.value));
// publish mapped changes when value changes
this.listen(value => s.publish(mapper(value)));
// return readonly so mapped state can't be published into
return s.readonly();
}

into(state: State<T>) {
this.listen(value => state.publish(value));
}
}

export class State<T = any> extends ReadonlyState<T> {
#state: { value: T };

constructor(value: T) {
const state = { value };
super(state);
this.#state = state;
return x && typeof x === "object" && StateSymbol in x;
}

/**
Expand All @@ -55,31 +40,55 @@ export class State<T = any> extends ReadonlyState<T> {

static merge<T, RefMap extends { [k: string]: State }>(
...states: [State<T> | RefMap, ...State<T>[]]
): State<T> | State<[number, T]> | State<MergedStateValue<RefMap>> {
): ReadonlyState<[number, T]> | ReadonlyState<MergedStateValue<RefMap>> {
if (State.isState(states[0])) {
const merged = new State<[number, T]>([0, states[0].value]);
for (let index = 0; index < states.length; index++) {
const state = states[index] as State<T>;
state.listen(updated => merged.publish([index, updated]));
}
return merged;
return merged.readonly();
} else {
const obj = states[0];
type MergedValue = MergedStateValue<RefMap>;
const merged = new State<MergedValue>(Object.values(obj)[0]?.value);
for (const key in obj) obj[key].listen(updated => merged.publish([key, updated]));
return merged;
return merged.readonly();
}
}

publish(next: T | Promise<T>) {
return Promise.resolve(next).then(val => {
this.#state.value = val;
this.subscribers.forEach(subscriber => subscriber(val));
this.#subscribers.forEach(subscriber => subscriber(val));
});
}

listen(listener: Subscriber<T>) {
this.#subscribers.push(listener);
}

map<U>(mapper: (t: T) => U): ReadonlyState<U> {
const s = new State(mapper(this.value));
// publish mapped changes when value changes
this.listen(value => s.publish(mapper(value)));
// return readonly so mapped state can't be published into
return s.readonly();
}

into(state: State<T>) {
this.listen(value => state.publish(value));
}

readonly() {
return new ReadonlyState(this.#state);
return {
get value(): T {
return this.value;
},
listen: this.listen.bind(this),
map: this.map.bind(this),
into: this.into.bind(this),
[StateSymbol]: true,
};
}
}

0 comments on commit b2982d4

Please sign in to comment.