Skip to content

Commit b2982d4

Browse files
committed
feat: ReadonlyState is now only an interfac
1 parent 90c89ad commit b2982d4

3 files changed

Lines changed: 46 additions & 38 deletions

File tree

hyper/node.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Tag } from "./lib/tags.ts";
22
import { EmptyElements } from "./lib/emptyElements.ts";
33
import { Attr } from "./lib/attributes.ts";
44
import { Falsy, isFalsy } from "./util.ts";
5-
import { State, ReadonlyState } from "./state.ts";
5+
import { State, type ReadonlyState } from "./state.ts";
66

77
export type NonEmptyElement = Exclude<Tag, EmptyElements>;
88

@@ -46,7 +46,6 @@ export type HyperNodeish<T extends Tag = Tag> =
4646
| HyperTextNode
4747
| HyperHTMLStringNode
4848
| Falsy
49-
| State<HyperNode<T> | HyperTextNode | HyperHTMLStringNode | Falsy>
5049
| ReadonlyState<HyperNode<T> | HyperTextNode | HyperHTMLStringNode | Falsy>;
5150

5251
// deno-lint-ignore no-explicit-any

hyper/render/dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { guessEnv } from "../guessEnv.ts";
22
import { Falsy, isFalsy } from "../util.ts";
3-
import { State, ReadonlyState } from "../state.ts";
3+
import { State, type ReadonlyState } from "../state.ts";
44
import { Document, HTMLElement, Node, Text } from "../lib/dom.ts";
55
import { HyperHTMLStringNode, HyperNodeish } from "../node.ts";
66

hyper/state.ts

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@ type MergedStateValue<Obj extends Record<string, State>> = {
44
[key in keyof Obj]: [key: key, value: Obj[key]["value"]];
55
}[keyof Obj];
66

7-
export class ReadonlyState<T = any> {
8-
protected subscribers: Subscriber<T>[] = [];
7+
export interface ReadonlyState<T = any> {
8+
value: T;
9+
listen(listener: Subscriber<T>): void;
10+
map<U>(mapper: (t: T) => U): ReadonlyState<U>;
11+
into(state: State<T>): void;
12+
}
13+
14+
const StateSymbol = Symbol("@hyperactive/state");
15+
16+
export class State<T = any> implements ReadonlyState<T> {
17+
#subscribers: Subscriber<T>[] = [];
918
#state: { value: T };
19+
[StateSymbol] = true;
1020

11-
constructor(state: { value: T }) {
21+
constructor(value: T) {
22+
const state = { value };
1223
this.#state = state;
1324
}
1425

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

1930
static isState<X>(x: X): x is Extract<X, State | ReadonlyState> {
20-
return x instanceof ReadonlyState;
21-
}
22-
23-
listen(listener: Subscriber<T>) {
24-
this.subscribers.push(listener);
25-
}
26-
27-
map<U>(mapper: (t: T) => U) {
28-
const s = new State(mapper(this.value));
29-
// publish mapped changes when value changes
30-
this.listen(value => s.publish(mapper(value)));
31-
// return readonly so mapped state can't be published into
32-
return s.readonly();
33-
}
34-
35-
into(state: State<T>) {
36-
this.listen(value => state.publish(value));
37-
}
38-
}
39-
40-
export class State<T = any> extends ReadonlyState<T> {
41-
#state: { value: T };
42-
43-
constructor(value: T) {
44-
const state = { value };
45-
super(state);
46-
this.#state = state;
31+
return x && typeof x === "object" && StateSymbol in x;
4732
}
4833

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

5641
static merge<T, RefMap extends { [k: string]: State }>(
5742
...states: [State<T> | RefMap, ...State<T>[]]
58-
): State<T> | State<[number, T]> | State<MergedStateValue<RefMap>> {
43+
): ReadonlyState<[number, T]> | ReadonlyState<MergedStateValue<RefMap>> {
5944
if (State.isState(states[0])) {
6045
const merged = new State<[number, T]>([0, states[0].value]);
6146
for (let index = 0; index < states.length; index++) {
6247
const state = states[index] as State<T>;
6348
state.listen(updated => merged.publish([index, updated]));
6449
}
65-
return merged;
50+
return merged.readonly();
6651
} else {
6752
const obj = states[0];
6853
type MergedValue = MergedStateValue<RefMap>;
6954
const merged = new State<MergedValue>(Object.values(obj)[0]?.value);
7055
for (const key in obj) obj[key].listen(updated => merged.publish([key, updated]));
71-
return merged;
56+
return merged.readonly();
7257
}
7358
}
7459

7560
publish(next: T | Promise<T>) {
7661
return Promise.resolve(next).then(val => {
7762
this.#state.value = val;
78-
this.subscribers.forEach(subscriber => subscriber(val));
63+
this.#subscribers.forEach(subscriber => subscriber(val));
7964
});
8065
}
8166

67+
listen(listener: Subscriber<T>) {
68+
this.#subscribers.push(listener);
69+
}
70+
71+
map<U>(mapper: (t: T) => U): ReadonlyState<U> {
72+
const s = new State(mapper(this.value));
73+
// publish mapped changes when value changes
74+
this.listen(value => s.publish(mapper(value)));
75+
// return readonly so mapped state can't be published into
76+
return s.readonly();
77+
}
78+
79+
into(state: State<T>) {
80+
this.listen(value => state.publish(value));
81+
}
82+
8283
readonly() {
83-
return new ReadonlyState(this.#state);
84+
return {
85+
get value(): T {
86+
return this.value;
87+
},
88+
listen: this.listen.bind(this),
89+
map: this.map.bind(this),
90+
into: this.into.bind(this),
91+
[StateSymbol]: true,
92+
};
8493
}
8594
}

0 commit comments

Comments
 (0)