1/**
2 * Mutable reactive references for local, in-memory state.
3 *
4 * `AtomRef` provides small observable state cells that can be read, updated,
5 * mapped, and subscribed to without going through an `AtomRegistry`. Mutable
6 * refs can also create refs for nested properties. The module also provides a
7 * collection helper that stores item refs and notifies subscribers when items are
8 * inserted, removed, or changed.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Equal from "../Equal.ts"
14import * as Hash from "../Hash.ts"
15
16/**
17 * The literal type used to identify `AtomRef` values.
18 *
19 * @stability unstable
20 * @category type IDs
21 * @since 4.0.0
22 */
23export type TypeId = "~effect/reactivity/AtomRef"
24
25/**
26 * The runtime type id used to identify `AtomRef` values.
27 *
28 * @stability unstable
29 * @category type IDs
30 * @since 4.0.0
31 */
32export const TypeId: TypeId = "~effect/reactivity/AtomRef"
33
34/**
35 * A read-only reactive reference.
36 *
37 * **Details**
38 *
39 * It exposes a stable key, the current value, subscriptions to value changes, and
40 * `map` for creating derived read-only references. Equality and hashing are based
41 * on the current value.
42 *
43 * @stability unstable
44 * @category models
45 * @since 4.0.0
46 */
47export interface ReadonlyRef<A> extends Equal.Equal {
48 readonly [TypeId]: TypeId
49 readonly key: string
50 readonly value: A
51 readonly subscribe: (f: (a: A) => void) => () => void
52 readonly map: <B>(f: (a: A) => B) => ReadonlyRef<B>
53}
54
55/**
56 * A mutable reactive reference.
57 *
58 * **Details**
59 *
60 * It supports replacing the whole value, updating it from the current value, and
61 * creating mutable references to nested properties.
62 *
63 * @stability unstable
64 * @category models
65 * @since 4.0.0
66 */
67export interface AtomRef<A> extends ReadonlyRef<A> {
68 readonly prop: <K extends keyof A>(prop: K) => AtomRef<A[K]>
69 readonly set: (value: A) => AtomRef<A>
70 readonly update: (f: (value: A) => A) => AtomRef<A>
71}
72
73/**
74 * A reactive collection of mutable item references.
75 *
76 * **Details**
77 *
78 * The collection can push, insert, and remove item refs, and `toArray` returns the
79 * current raw item values.
80 *
81 * @stability unstable
82 * @category models
83 * @since 4.0.0
84 */
85export interface Collection<A> extends ReadonlyRef<ReadonlyArray<AtomRef<A>>> {
86 readonly push: (item: A) => Collection<A>
87 readonly insertAt: (index: number, item: A) => Collection<A>
88 readonly remove: (ref: AtomRef<A>) => Collection<A>
89 readonly toArray: () => Array<A>
90}
91
92/**
93 * Creates a mutable reactive reference initialized with the supplied value.
94 *
95 * @stability unstable
96 * @category constructors
97 * @since 4.0.0
98 */
99export const make = <A>(value: A): AtomRef<A> => new AtomRefImpl(value)
100
101/**
102 * Creates a reactive collection from an iterable of initial item values.
103 *
104 * **Details**
105 *
106 * Each item is wrapped in an `AtomRef`, and changes to item refs notify the
107 * collection subscribers.
108 *
109 * @stability unstable
110 * @category constructors
111 * @since 4.0.0
112 */
113export const collection = <A>(items: Iterable<A>): Collection<A> => new CollectionImpl(items)
114
115const keyState = {
116 count: 0,
117 generate() {
118 return `AtomRef-${this.count++}`
119 }
120}
121
122class ReadonlyRefImpl<A> implements ReadonlyRef<A> {
123 readonly [TypeId]: TypeId
124 readonly key = keyState.generate()
125 public value: A
126 constructor(value: A) {
127 this[TypeId] = TypeId
128 this.value = value
129 }
130
131 [Equal.symbol](that: Equal.Equal) {
132 return Equal.equals(this.value, (that as ReadonlyRef<A>).value)
133 }
134
135 [Hash.symbol]() {
136 return Hash.hash(this.value)
137 }
138
139 listeners: Listener<A> | null = null
140
141 notify(a: A) {
142 let listener = this.listeners
143 while (listener !== null) {
144 listener.f(a)
145 listener = listener.next
146 }
147 }
148
149 subscribe(f: (a: A) => void): () => void {
150 const listener: Listener<A> = {
151 f,
152 prev: null,
153 next: this.listeners
154 }
155 if (this.listeners) {
156 this.listeners.prev = listener
157 }
158 this.listeners = listener
159
160 return () => {
161 if (this.listeners === listener) {
162 this.listeners = listener.next
163 }
164 if (listener.prev) {
165 listener.prev.next = listener.next
166 }
167 if (listener.next) {
168 listener.next.prev = listener.prev
169 }
170 }
171 }
172
173 map<B>(f: (a: A) => B): ReadonlyRef<B> {
174 return new MapRefImpl(this, f)
175 }
176}
177
178type Listener<A> = {
179 readonly f: (a: A) => void
180 prev: Listener<A> | null
181 next: Listener<A> | null
182}
183
184class AtomRefImpl<A> extends ReadonlyRefImpl<A> implements AtomRef<A> {
185 prop<K extends keyof A>(prop: K): AtomRef<A[K]> {
186 return new PropRefImpl(this, prop)
187 }
188 set(value: A) {
189 if (Equal.equals(value, this.value)) {
190 return this
191 }
192 this.value = value
193 this.notify(value)
194 return this
195 }
196
197 update(f: (value: A) => A) {
198 return this.set(f(this.value))
199 }
200}
201
202class MapRefImpl<A, B> implements ReadonlyRef<B> {
203 readonly [TypeId]: TypeId
204 readonly key = keyState.generate()
205 readonly parent: ReadonlyRef<A>
206 readonly transform: (a: A) => B
207 constructor(parent: ReadonlyRef<A>, transform: (a: A) => B) {
208 this[TypeId] = TypeId
209 this.parent = parent
210 this.transform = transform
211 }
212 [Equal.symbol](that: Equal.Equal) {
213 return Equal.equals(this.value, (that as ReadonlyRef<B>).value)
214 }
215 [Hash.symbol]() {
216 return Hash.hash(this.value)
217 }
218 get value() {
219 return this.transform(this.parent.value)
220 }
221 subscribe(f: (a: B) => void): () => void {
222 let previous = this.transform(this.parent.value)
223 return this.parent.subscribe((a) => {
224 const next = this.transform(a)
225 if (Equal.equals(next, previous)) {
226 return
227 }
228 previous = next
229 f(next)
230 })
231 }
232 map<C>(f: (a: B) => C): ReadonlyRef<C> {
233 return new MapRefImpl(this, f)
234 }
235}
236
237class PropRefImpl<A, K extends keyof A> implements AtomRef<A[K]> {
238 readonly [TypeId]: TypeId
239 readonly key = keyState.generate()
240 private previous: A[K]
241 readonly parent: AtomRef<A>
242 readonly _prop: K
243
244 constructor(parent: AtomRef<A>, _prop: K) {
245 this[TypeId] = TypeId
246 this.parent = parent
247 this._prop = _prop
248 this.previous = parent.value[_prop]
249 }
250 [Equal.symbol](that: Equal.Equal) {
251 return Equal.equals(this.value, (that as ReadonlyRef<A>).value)
252 }
253 [Hash.symbol]() {
254 return Hash.hash(this.value)
255 }
256 get value() {
257 if (this.parent.value && this._prop in (this.parent.value as any)) {
258 this.previous = this.parent.value[this._prop]
259 }
260 return this.previous
261 }
262 subscribe(f: (a: A[K]) => void): () => void {
263 let previous = this.value
264 return this.parent.subscribe((a) => {
265 if (!a || !(this._prop in (a as any))) {
266 return
267 }
268 const next = a[this._prop]
269 if (Equal.equals(next, previous)) {
270 return
271 }
272 previous = next
273 f(next)
274 })
275 }
276 map<C>(f: (a: A[K]) => C): ReadonlyRef<C> {
277 return new MapRefImpl(this, f)
278 }
279 prop<CK extends keyof A[K]>(prop: CK): AtomRef<A[K][CK]> {
280 return new PropRefImpl(this, prop)
281 }
282 set(value: A[K]): AtomRef<A[K]> {
283 if (Array.isArray(this.parent.value)) {
284 const newArray = this.parent.value.slice()
285 newArray[this._prop as number] = value
286 this.parent.set(newArray as A)
287 } else {
288 this.parent.set({
289 ...this.parent.value,
290 [this._prop]: value
291 })
292 }
293 return this
294 }
295 update(f: (value: A[K]) => A[K]): AtomRef<A[K]> {
296 if (Array.isArray(this.parent.value)) {
297 const newArray = this.parent.value.slice()
298 newArray[this._prop as number] = f(this.parent.value[this._prop])
299 this.parent.set(newArray as A)
300 } else {
301 this.parent.set({
302 ...this.parent.value,
303 [this._prop]: f(this.parent.value[this._prop])
304 })
305 }
306 return this
307 }
308}
309
310class CollectionImpl<A> extends ReadonlyRefImpl<Array<AtomRef<A>>> implements Collection<A> {
311 constructor(items: Iterable<A>) {
312 super([])
313 for (const item of items) {
314 this.value.push(this.makeRef(item))
315 }
316 }
317
318 makeRef(value: A) {
319 const ref = new AtomRefImpl(value)
320 const notify = (value: A) => {
321 ref.notify(value)
322 this.notify(this.value)
323 }
324 return new Proxy(ref, {
325 get(target, p, _receiver) {
326 if (p === "notify") {
327 return notify
328 }
329 return target[p as keyof AtomRef<A>]
330 }
331 })
332 }
333
334 push(item: A) {
335 const ref = this.makeRef(item)
336 this.value.push(ref)
337 this.notify(this.value)
338 return this
339 }
340
341 insertAt(index: number, item: A) {
342 const ref = this.makeRef(item)
343 this.value.splice(index, 0, ref)
344 this.notify(this.value)
345 return this
346 }
347
348 remove(ref: AtomRef<A>) {
349 const index = this.value.indexOf(ref)
350 if (index !== -1) {
351 this.value.splice(index, 1)
352 this.notify(this.value)
353 }
354 return this
355 }
356
357 toArray() {
358 return this.value.map((ref) => ref.value)
359 }
360}