@bodil/signal 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bodil/signal",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Preact Signals like signal implementation built on the TC39 Signals Proposal",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,8 @@
10
10
  "module": "dist/index.js",
11
11
  "types": "dist/index.d.ts",
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "src"
14
15
  ],
15
16
  "exports": {
16
17
  ".": {
package/src/index.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { Signal } from "signal-polyfill";
2
+ import { toDisposable, type Disposifiable } from "@bodil/disposable";
3
+ import { Err, Ok, type Result, Async } from "@bodil/core";
4
+
5
+ interface ReadableSignal<A> {
6
+ readonly value: A;
7
+ map<B>(fn: (value: A) => B): ComputedSignal<B>;
8
+ on(callback: (value: A) => void): Disposable;
9
+ }
10
+
11
+ export class StateSignal<A> extends Signal.State<A> implements ReadableSignal<A> {
12
+ get value(): A {
13
+ return this.get();
14
+ }
15
+
16
+ set value(value: A) {
17
+ this.set(value);
18
+ }
19
+
20
+ update(fn: (value: A) => A): void {
21
+ this.set(Signal.subtle.untrack(() => fn(this.get())));
22
+ }
23
+
24
+ readOnly(): ComputedSignal<A> {
25
+ return computed(() => this.get());
26
+ }
27
+
28
+ map<B>(fn: (value: A) => B): ComputedSignal<B> {
29
+ return computed(() => fn(this.get()));
30
+ }
31
+
32
+ on(callback: (value: A) => void): Disposable {
33
+ return subscribe(this, callback);
34
+ }
35
+ }
36
+
37
+ export class ComputedSignal<A> extends Signal.Computed<A> implements ReadableSignal<A> {
38
+ get value(): A {
39
+ return this.get();
40
+ }
41
+
42
+ map<B>(fn: (value: A) => B): ComputedSignal<B> {
43
+ return computed(() => fn(this.get()));
44
+ }
45
+
46
+ on(callback: (value: A) => void): Disposable {
47
+ return subscribe(this, callback);
48
+ }
49
+ }
50
+
51
+ type AnySignal<A> = StateSignal<A> | ComputedSignal<A>;
52
+
53
+ export type { AnySignal as Signal };
54
+
55
+ export const subtle = Signal.subtle;
56
+
57
+ export function signal<A>(value: A, options?: Signal.Options<A>): StateSignal<A> {
58
+ return new StateSignal(value, options);
59
+ }
60
+
61
+ export function computed<A>(
62
+ fn: (this: ComputedSignal<A>) => A,
63
+ options?: Signal.Options<A>
64
+ ): ComputedSignal<A> {
65
+ return new ComputedSignal(fn, options);
66
+ }
67
+
68
+ let effectNeedsEnqueue = true;
69
+ const effectWatcher = new Signal.subtle.Watcher(() => {
70
+ if (effectNeedsEnqueue) {
71
+ effectNeedsEnqueue = false;
72
+ queueMicrotask(effectProcess);
73
+ }
74
+ });
75
+
76
+ function effectProcess(): void {
77
+ effectNeedsEnqueue = true;
78
+ for (const sig of effectWatcher.getPending()) {
79
+ sig.get();
80
+ }
81
+ effectWatcher.watch();
82
+ }
83
+
84
+ export function effect(fn: () => Disposifiable | void): Disposable {
85
+ let cleanup: Disposable | undefined;
86
+ const computed = new ComputedSignal(() => {
87
+ if (cleanup !== undefined) {
88
+ cleanup[Symbol.dispose]();
89
+ }
90
+ const result = fn();
91
+ cleanup = result !== undefined ? toDisposable(result) : undefined;
92
+ });
93
+ effectWatcher.watch(computed);
94
+ computed.get();
95
+ return toDisposable(() => {
96
+ effectWatcher.unwatch(computed);
97
+ if (cleanup !== undefined) {
98
+ cleanup[Symbol.dispose]();
99
+ }
100
+ });
101
+ }
102
+
103
+ export function subscribe<A>(signal: ReadableSignal<A>, callback: (value: A) => void): Disposable {
104
+ return effect(() => callback(signal.value));
105
+ }
106
+
107
+ export function asyncComputed<A>(
108
+ fn: (abort: AbortSignal) => Promise<A>,
109
+ options?: Signal.Options<A>
110
+ ): Promise<ComputedSignal<A>> {
111
+ const result = Promise.withResolvers<ComputedSignal<A>>();
112
+ const stream = computed(() => Async.abortable(fn));
113
+ const sig: StateSignal<Result<A, Error>> = signal(Err(new Error()));
114
+ let job: Async.AbortableJob<A> | undefined = undefined;
115
+ let resolved = false;
116
+ const resolve = () => {
117
+ if (!resolved) {
118
+ resolved = true;
119
+ result.resolve(computed(() => sig.get().unwrapExact(), options));
120
+ }
121
+ };
122
+ effect(() => {
123
+ if (job !== undefined) {
124
+ job.abort();
125
+ }
126
+ job = stream.get();
127
+ job.result.then(
128
+ (next) => {
129
+ sig.set(Ok(next));
130
+ resolve();
131
+ },
132
+ (error) => {
133
+ if (job?.signal.aborted === true) {
134
+ return;
135
+ }
136
+ sig.set(Err(error));
137
+ resolve();
138
+ }
139
+ );
140
+ });
141
+ return result.promise;
142
+ }
@@ -0,0 +1,77 @@
1
+ import { sleep } from "@bodil/core/async";
2
+ import { expect, test } from "vitest";
3
+ import { asyncComputed, effect, signal } from ".";
4
+
5
+ test("Signal", async () => {
6
+ const sig = signal(0);
7
+ const result: Array<number> = [];
8
+ effect(() => void result.push(sig.value));
9
+ expect(result).toEqual([0]);
10
+ sig.value = 1;
11
+ sig.value = 2;
12
+ sig.value = 3;
13
+ expect(result).toEqual([0]);
14
+ const done = Promise.withResolvers<void>();
15
+ setTimeout(() => {
16
+ try {
17
+ expect(result).toEqual([0, 3]);
18
+ done.resolve();
19
+ } catch (e) {
20
+ done.reject(e as Error);
21
+ }
22
+ }, 1);
23
+ await done.promise;
24
+ });
25
+
26
+ test("signal equality", async () => {
27
+ const result: Array<number> = [];
28
+ const sig = signal(0);
29
+ effect(() => void result.push(sig.value));
30
+ expect(result).toEqual([0]);
31
+ sig.value = 1;
32
+ await sleep(1);
33
+ expect(result).toEqual([0, 1]);
34
+ sig.value = 1;
35
+ await sleep(1);
36
+ expect(result).toEqual([0, 1]);
37
+ sig.value = 2;
38
+ await sleep(1);
39
+ expect(result).toEqual([0, 1, 2]);
40
+ sig.value = 2;
41
+ await sleep(1);
42
+ expect(result).toEqual([0, 1, 2]);
43
+ sig.value = 3;
44
+ await sleep(1);
45
+ expect(result).toEqual([0, 1, 2, 3]);
46
+ sig.value = 3;
47
+ await sleep(1);
48
+ expect(result).toEqual([0, 1, 2, 3]);
49
+ sig.value = 2;
50
+ await sleep(1);
51
+ expect(result).toEqual([0, 1, 2, 3, 2]);
52
+ });
53
+
54
+ test("asyncComputed", async () => {
55
+ const s = signal(1);
56
+ const c = await asyncComputed(() => Promise.resolve(s.value + 1));
57
+ expect(c.value).toEqual(2);
58
+ try {
59
+ await asyncComputed(() => {
60
+ throw new Error("welp!");
61
+ });
62
+ throw new Error("computed error failed to throw");
63
+ } catch (error) {
64
+ expect(error).toBeInstanceOf(Error);
65
+ expect((error as Error).message).toEqual("welp!");
66
+ }
67
+ s.value = 2;
68
+ expect(c.value).toEqual(2);
69
+ await sleep(1);
70
+ expect(c.value).toEqual(3);
71
+ s.value = 1;
72
+ s.value = 2;
73
+ s.value = 3;
74
+ expect(c.value).toEqual(3);
75
+ await sleep(1);
76
+ expect(c.value).toEqual(4);
77
+ });