@bodil/signal 0.3.5 → 0.4.0

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.
@@ -0,0 +1,62 @@
1
+ import { None, Some } from "@bodil/opt";
2
+ import { expect, test } from "vitest";
3
+
4
+ import { Signal } from ".";
5
+
6
+ test("SignalMap", () => {
7
+ const map = Signal.map<string, string>([
8
+ ["Joe", "Armstrong"],
9
+ ["Mike", "Williams"],
10
+ ["Robert", "Virding"],
11
+ ]);
12
+
13
+ expect(map.entries().toArray()).toEqual([
14
+ ["Joe", "Armstrong"],
15
+ ["Mike", "Williams"],
16
+ ["Robert", "Virding"],
17
+ ]);
18
+
19
+ const mike = map.signal("Mike");
20
+ expect(mike.get()).toEqual(Some("Williams"));
21
+
22
+ const bjarne = map.signal("Bjarne");
23
+ expect(bjarne.get()).toEqual(None);
24
+
25
+ const size = Signal.computed(() => map.size());
26
+ expect(map.size()).toBe(3);
27
+ expect(size.get()).toBe(3);
28
+ expect(mike.get()).toEqual(Some("Williams"));
29
+ expect(bjarne.get()).toEqual(None);
30
+
31
+ map.set("Bjarne", "Däcker");
32
+ expect(map.size()).toBe(4);
33
+ expect(size.get()).toBe(4);
34
+ expect(mike.get()).toEqual(Some("Williams"));
35
+ expect(bjarne.get()).toEqual(Some("Däcker"));
36
+
37
+ expect(map.entries().toArray()).toEqual([
38
+ ["Joe", "Armstrong"],
39
+ ["Mike", "Williams"],
40
+ ["Robert", "Virding"],
41
+ ["Bjarne", "Däcker"],
42
+ ]);
43
+
44
+ expect(map.remove("Mike")).toEqual(Some("Williams"));
45
+ expect(map.size()).toBe(3);
46
+ expect(size.get()).toBe(3);
47
+ expect(mike.get()).toEqual(None);
48
+ expect(bjarne.get()).toEqual(Some("Däcker"));
49
+
50
+ expect(map.entries().toArray()).toEqual([
51
+ ["Joe", "Armstrong"],
52
+ ["Robert", "Virding"],
53
+ ["Bjarne", "Däcker"],
54
+ ]);
55
+
56
+ map.clear();
57
+ expect(map.size()).toBe(0);
58
+ expect(size.get()).toBe(0);
59
+ expect(mike.get()).toEqual(None);
60
+ expect(bjarne.get()).toEqual(None);
61
+ expect(map.entries().toArray()).toEqual([]);
62
+ });
package/src/map.ts ADDED
@@ -0,0 +1,263 @@
1
+ import { present } from "@bodil/core/assert";
2
+ import type { Equals } from "@bodil/core/types";
3
+ import { None, Some, type Option } from "@bodil/opt";
4
+
5
+ import * as Signal from "./signal";
6
+
7
+ const instance = Symbol("Signal.Map");
8
+
9
+ type InstanceProps<K, V> = {
10
+ map: Map<K, V>;
11
+ signal: Signal.State<null>;
12
+ signals: Map<K, WeakRef<Signal.State<Option<V>>>>;
13
+ readonlySignals: WeakMap<Signal.State<Option<V>>, Signal.Computed<Option<V>>>;
14
+ registry: FinalizationRegistry<K>;
15
+ };
16
+
17
+ /**
18
+ * A read-only view of a {@link Signal.Map}.
19
+ *
20
+ * This acts just like the {@link Signal.Map} it was created from, mirroring
21
+ * the current contents of the source map, but without the ability to modify
22
+ * it.
23
+ */
24
+ export class ReadonlySignalMap<K, V> implements Iterable<[K, V]>, Equals {
25
+ /** @ignore */
26
+ protected readonly [instance]: InstanceProps<K, V>;
27
+
28
+ /** @ignore */
29
+ protected constructor(instanceProps: InstanceProps<K, V>) {
30
+ this[instance] = instanceProps;
31
+ }
32
+
33
+ /**
34
+ * Test whether a value is a `Signal.ReadonlyMap`.
35
+ */
36
+ static is(value: unknown): value is ReadonlySignalMap<unknown, unknown> {
37
+ return value instanceof ReadonlySignalMap;
38
+ }
39
+
40
+ /**
41
+ * Obtain a {@link Signal.Computed} representing the current value of the
42
+ * given key in the map. The signal will update when the value changes, and
43
+ * will not respond to changes to any other elements of the map.
44
+ */
45
+ signal(key: K): Signal.Computed<Option<V>> {
46
+ const self = this[instance];
47
+ const active = self.signals.get(key)?.deref();
48
+ if (active !== undefined) {
49
+ return present(
50
+ self.readonlySignals.get(active),
51
+ "Signal.Map.signal(): unexpected absence of readonly signal for a signal which still exists",
52
+ );
53
+ }
54
+ const sig = new Signal.State(self.map.has(key) ? Some(self.map.get(key)!) : None);
55
+ self.registry.register(sig, key);
56
+ self.signals.set(key, new WeakRef(sig));
57
+ const roSig = sig.readOnly();
58
+ self.readonlySignals.set(sig, roSig);
59
+ return roSig;
60
+ }
61
+
62
+ /**
63
+ * Get the value associated with the given key in the map.
64
+ */
65
+ get(key: K): Option<V> {
66
+ return this.has(key) ? Some(this[instance].map.get(key)!) : None;
67
+ }
68
+
69
+ /**
70
+ * Test whether the given key is defined in the map.
71
+ */
72
+ has(key: K): boolean {
73
+ const self = this[instance];
74
+ self.signal.get();
75
+ return self.map.has(key);
76
+ }
77
+
78
+ /**
79
+ * Get the size of the map.
80
+ */
81
+ size(): number {
82
+ const self = this[instance];
83
+ self.signal.get();
84
+ return self.map.size;
85
+ }
86
+
87
+ /**
88
+ * Test whether the map is empty.
89
+ */
90
+ isEmpty(): boolean {
91
+ return this.size() === 0;
92
+ }
93
+
94
+ /**
95
+ * Iterate over the key/value pairs the map contains.
96
+ */
97
+ entries(): MapIterator<[K, V]> {
98
+ const self = this[instance];
99
+ self.signal.get();
100
+ return self.map.entries();
101
+ }
102
+
103
+ /**
104
+ * Iterate over the keys the map contains.
105
+ */
106
+ keys(): MapIterator<K> {
107
+ const self = this[instance];
108
+ self.signal.get();
109
+ return self.map.keys();
110
+ }
111
+
112
+ /**
113
+ * Iterate over the values associated with every key in the map.
114
+ */
115
+ values(): MapIterator<V> {
116
+ const self = this[instance];
117
+ self.signal.get();
118
+ return self.map.values();
119
+ }
120
+
121
+ /**
122
+ * Test whether a given value is a `Signal.Map` with identical contents to
123
+ * this map.
124
+ */
125
+ equals(other: unknown): other is ReadonlySignalMap<K, V> {
126
+ if (!ReadonlySignalMap.is(other) || this.size() !== other.size()) {
127
+ return false;
128
+ }
129
+ for (const [key, value] of this) {
130
+ const otherValue = other.get(key);
131
+ if (otherValue.isNone() || !Object.is(value, otherValue.value)) {
132
+ return false;
133
+ }
134
+ }
135
+ return true;
136
+ }
137
+
138
+ /**
139
+ * Iterate over the key/value pairs the map contains.
140
+ */
141
+ [Symbol.iterator](): MapIterator<[K, V]> {
142
+ const self = this[instance];
143
+ self.signal.get();
144
+ return self.map[Symbol.iterator]();
145
+ }
146
+ }
147
+
148
+ /**
149
+ * A map which behaves like a signal.
150
+ *
151
+ * Any read operation on the map, like {@link Signal.Map.get},
152
+ * {@link Signal.Map.size}, or iteration, acts like a
153
+ * {@link Signal.State.get}, registering the map as a dependency in any
154
+ * computed signals or effects it's used in. Correspondingly, any write
155
+ * operation to the map will cause all of these dependencies to update as
156
+ * with a {@link Signal.State.set}.
157
+ *
158
+ * Note that there's no granularity to this access by default: if you read only
159
+ * a single key from the map inside a computed signal, *any* write to the map,
160
+ * even to a different key, causes the signal to recompute. If you want
161
+ * granularity (and you normally do), use {@link Signal.Map.signal} instead of
162
+ * {@link Signal.Map.get}.
163
+ */
164
+ export class SignalMap<K, V> extends ReadonlySignalMap<K, V> {
165
+ /**
166
+ * Test whether a value is a `Signal.Map`.
167
+ */
168
+ static is(value: unknown): value is SignalMap<unknown, unknown> {
169
+ return value instanceof SignalMap;
170
+ }
171
+
172
+ /**
173
+ * Construct a new map, optionally populating it with the provided key/value
174
+ * pairs.
175
+ */
176
+ constructor(entries: Iterable<readonly [K, V]> = []) {
177
+ const signals = new Map<K, WeakRef<Signal.State<Option<V>>>>();
178
+ super({
179
+ map: new Map(entries),
180
+ signal: new Signal.State(null, { equals: () => false }),
181
+ signals,
182
+ readonlySignals: new WeakMap(),
183
+ registry: new FinalizationRegistry((key) => signals.delete(key)),
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Discard every key/value pair defined in the map, leaving an empty map.
189
+ */
190
+ clear(): void {
191
+ const self = this[instance];
192
+ self.map.clear();
193
+ self.signals.forEach((sig) => sig.deref()?.set(None));
194
+ self.signal.set(null);
195
+ }
196
+
197
+ /**
198
+ * Remove the given key from the map, returning its associated value. If the
199
+ * key isn't defined in the map, return {@link None}.
200
+ */
201
+ remove(key: K): Option<V> {
202
+ const self = this[instance];
203
+ if (!self.map.has(key)) {
204
+ return None;
205
+ }
206
+ const result = Some(self.map.get(key)!);
207
+ self.map.delete(key);
208
+ self.signals.get(key)?.deref()?.set(None);
209
+ self.signal.set(null);
210
+ return result;
211
+ }
212
+
213
+ /**
214
+ * Add the given key/value pair to the map, overwriting any previous
215
+ * definitions of the same key.
216
+ */
217
+ set(key: K, value: V): this {
218
+ const self = this[instance];
219
+ self.map.set(key, value);
220
+ self.signals.get(key)?.deref()?.set(Some(value));
221
+ self.signal.set(null);
222
+ return this;
223
+ }
224
+
225
+ /**
226
+ * Get the value associated with the given key, or, if the key doesn't
227
+ * already exist, call the provided function to create a new value and
228
+ * insert it into the map under the given key.
229
+ */
230
+ getOrSet(key: K, defaultFn: (this: SignalMap<K, V>) => V): V {
231
+ const self = this[instance];
232
+ self.signal.get();
233
+ if (self.map.has(key)) {
234
+ return self.map.get(key)!;
235
+ }
236
+ const value = defaultFn.call(this);
237
+ self.map.set(key, value);
238
+ self.signal.set(null);
239
+ return value;
240
+ }
241
+
242
+ /**
243
+ * Get a signal reflecting the value at the given key, as in
244
+ * {@link Signal.Map.signal}, except that if the key isn't defined,
245
+ * initialise it with the value produced by calling `defaultFn` first, as in
246
+ * {@link Signal.Map.getOrSet}.
247
+ */
248
+ signalOrSet(key: K, defaultFn: (this: SignalMap<K, V>) => V): Signal.Computed<Option<V>> {
249
+ const self = this[instance];
250
+ if (!self.map.has(key)) {
251
+ self.map.set(key, defaultFn.call(this));
252
+ self.signal.set(null);
253
+ }
254
+ return this.signal(key);
255
+ }
256
+
257
+ /**
258
+ * Return a read-only view of the map.
259
+ */
260
+ readOnly(): ReadonlySignalMap<K, V> {
261
+ return new ReadonlySignalMap(this[instance]);
262
+ }
263
+ }
@@ -4,7 +4,7 @@ import { expect, test } from "vitest";
4
4
  import { Signal } from ".";
5
5
 
6
6
  test("Signal", async () => {
7
- const sig = Signal(0);
7
+ const sig = Signal.state(0);
8
8
  const result: Array<number> = [];
9
9
  Signal.effect(() => void result.push(sig.value));
10
10
  expect(result).toEqual([0]);
@@ -26,7 +26,7 @@ test("Signal", async () => {
26
26
 
27
27
  test("signal equality", async () => {
28
28
  const result: Array<number> = [];
29
- const sig = Signal(0);
29
+ const sig = Signal.state(0);
30
30
  Signal.effect(() => void result.push(sig.value));
31
31
  expect(result).toEqual([0]);
32
32
  sig.value = 1;
@@ -53,7 +53,7 @@ test("signal equality", async () => {
53
53
  });
54
54
 
55
55
  test("asyncComputed", async () => {
56
- const s = Signal(1);
56
+ const s = Signal.state(1);
57
57
  const c = await Signal.asyncComputed(() => Promise.resolve(s.value + 1));
58
58
  expect(c.value).toEqual(2);
59
59
 
@@ -78,7 +78,7 @@ test("asyncComputed", async () => {
78
78
  });
79
79
 
80
80
  test("isSignal", () => {
81
- const s1: Signal.State<number> = Signal(1);
81
+ const s1: Signal.State<number> = Signal.state(1);
82
82
  expect(Signal.is(s1)).toBeTruthy();
83
83
  expect(Signal.State.is(s1)).toBeTruthy();
84
84
  expect(Signal.Computed.is(s1)).toBeFalsy();
@@ -92,11 +92,11 @@ test("isSignal", () => {
92
92
  });
93
93
 
94
94
  test("Signal types", () => {
95
- const s1: Signal.State<number> = Signal(1);
95
+ const s1: Signal.State<number> = Signal.state(1);
96
96
  const s2: Signal.Computed<number> = Signal.computed(() => s1.get());
97
97
 
98
- function foo(_signal: Signal<unknown>) {
99
- // ensure Signal<A> is an accessible type
98
+ function foo(_signal: Signal.Any<unknown>) {
99
+ // ensure Signal.Any<A> is an accessible type
100
100
  // and that it accepts both concrete signal types.
101
101
  }
102
102
  foo(s1);