@effect-atom/atom 0.1.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.
Files changed (65) hide show
  1. package/Atom/package.json +6 -0
  2. package/AtomRef/package.json +6 -0
  3. package/Hydration/package.json +6 -0
  4. package/LICENSE +21 -0
  5. package/README.md +3 -0
  6. package/Registry/package.json +6 -0
  7. package/Result/package.json +6 -0
  8. package/dist/cjs/Atom.js +1079 -0
  9. package/dist/cjs/Atom.js.map +1 -0
  10. package/dist/cjs/AtomRef.js +261 -0
  11. package/dist/cjs/AtomRef.js.map +1 -0
  12. package/dist/cjs/Hydration.js +100 -0
  13. package/dist/cjs/Hydration.js.map +1 -0
  14. package/dist/cjs/Registry.js +128 -0
  15. package/dist/cjs/Registry.js.map +1 -0
  16. package/dist/cjs/Result.js +454 -0
  17. package/dist/cjs/Result.js.map +1 -0
  18. package/dist/cjs/index.js +37 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/internal/registry.js +701 -0
  21. package/dist/cjs/internal/registry.js.map +1 -0
  22. package/dist/cjs/internal/runtime.js +92 -0
  23. package/dist/cjs/internal/runtime.js.map +1 -0
  24. package/dist/dts/Atom.d.ts +597 -0
  25. package/dist/dts/Atom.d.ts.map +1 -0
  26. package/dist/dts/AtomRef.d.ts +55 -0
  27. package/dist/dts/AtomRef.d.ts.map +1 -0
  28. package/dist/dts/Hydration.d.ts +27 -0
  29. package/dist/dts/Hydration.d.ts.map +1 -0
  30. package/dist/dts/Registry.d.ts +115 -0
  31. package/dist/dts/Registry.d.ts.map +1 -0
  32. package/dist/dts/Result.d.ts +351 -0
  33. package/dist/dts/Result.d.ts.map +1 -0
  34. package/dist/dts/index.d.ts +21 -0
  35. package/dist/dts/index.d.ts.map +1 -0
  36. package/dist/dts/internal/registry.d.ts +2 -0
  37. package/dist/dts/internal/registry.d.ts.map +1 -0
  38. package/dist/dts/internal/runtime.d.ts +2 -0
  39. package/dist/dts/internal/runtime.d.ts.map +1 -0
  40. package/dist/esm/Atom.js +1029 -0
  41. package/dist/esm/Atom.js.map +1 -0
  42. package/dist/esm/AtomRef.js +232 -0
  43. package/dist/esm/AtomRef.js.map +1 -0
  44. package/dist/esm/Hydration.js +71 -0
  45. package/dist/esm/Hydration.js.map +1 -0
  46. package/dist/esm/Registry.js +98 -0
  47. package/dist/esm/Registry.js.map +1 -0
  48. package/dist/esm/Result.js +403 -0
  49. package/dist/esm/Result.js.map +1 -0
  50. package/dist/esm/index.js +21 -0
  51. package/dist/esm/index.js.map +1 -0
  52. package/dist/esm/internal/registry.js +672 -0
  53. package/dist/esm/internal/registry.js.map +1 -0
  54. package/dist/esm/internal/runtime.js +64 -0
  55. package/dist/esm/internal/runtime.js.map +1 -0
  56. package/dist/esm/package.json +4 -0
  57. package/package.json +72 -0
  58. package/src/Atom.ts +1865 -0
  59. package/src/AtomRef.ts +282 -0
  60. package/src/Hydration.ts +98 -0
  61. package/src/Registry.ts +204 -0
  62. package/src/Result.ts +767 -0
  63. package/src/index.ts +24 -0
  64. package/src/internal/registry.ts +810 -0
  65. package/src/internal/runtime.ts +63 -0
@@ -0,0 +1,810 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Equal from "effect/Equal"
3
+ import * as Exit from "effect/Exit"
4
+ import { pipe } from "effect/Function"
5
+ import { globalValue } from "effect/GlobalValue"
6
+ import * as Option from "effect/Option"
7
+ import * as Queue from "effect/Queue"
8
+ import * as Stream from "effect/Stream"
9
+ import type * as Atom from "../Atom.js"
10
+ import type * as Registry from "../Registry.js"
11
+ import * as Result from "../Result.js"
12
+
13
+ const constImmediate = { immediate: true }
14
+ function constListener(_: any) {}
15
+
16
+ /** @internal */
17
+ export const TypeId: Registry.TypeId = "~effect-atom/atom/Registry"
18
+
19
+ /** @internal */
20
+ export const make = (options?: {
21
+ readonly initialValues?: Iterable<readonly [Atom.Atom<any>, any]> | undefined
22
+ readonly scheduleTask?: ((f: () => void) => void) | undefined
23
+ readonly timeoutResolution?: number | undefined
24
+ readonly defaultIdleTTL?: number | undefined
25
+ }): Registry.Registry =>
26
+ new RegistryImpl(
27
+ options?.initialValues,
28
+ options?.scheduleTask,
29
+ options?.timeoutResolution,
30
+ options?.defaultIdleTTL
31
+ )
32
+
33
+ const SerializableTypeId: Atom.SerializableTypeId = "~effect-atom/atom/Atom/Serializable"
34
+ const atomKey = <A>(atom: Atom.Atom<A>): Atom.Atom<A> | string =>
35
+ SerializableTypeId in atom ? (atom as Atom.Serializable)[SerializableTypeId].key : atom
36
+
37
+ class RegistryImpl implements Registry.Registry {
38
+ readonly [TypeId]: Registry.TypeId
39
+ readonly timeoutResolution: number
40
+
41
+ constructor(
42
+ initialValues?: Iterable<readonly [Atom.Atom<any>, any]>,
43
+ readonly scheduleTask = (cb: () => void): void => queueMicrotask(cb),
44
+ timeoutResolution?: number,
45
+ readonly defaultIdleTTL?: number
46
+ ) {
47
+ this[TypeId] = TypeId
48
+ if (timeoutResolution === undefined && defaultIdleTTL !== undefined) {
49
+ this.timeoutResolution = Math.round(defaultIdleTTL / 2)
50
+ } else {
51
+ this.timeoutResolution = timeoutResolution ?? 1000
52
+ }
53
+ if (initialValues !== undefined) {
54
+ for (const [atom, value] of initialValues) {
55
+ this.ensureNode(atom).setValue(value)
56
+ }
57
+ }
58
+ }
59
+
60
+ readonly nodes = new Map<Atom.Atom<any> | string, Node<any>>()
61
+ readonly preloadedSerializable = new Map<string, unknown>()
62
+ readonly timeoutBuckets = new Map<number, readonly [nodes: Set<Node<any>>, handle: number]>()
63
+ readonly nodeTimeoutBucket = new Map<Node<any>, number>()
64
+ disposed = false
65
+
66
+ getNodes() {
67
+ return this.nodes
68
+ }
69
+
70
+ get<A>(atom: Atom.Atom<A>): A {
71
+ return this.ensureNode(atom).value()
72
+ }
73
+
74
+ set<R, W>(atom: Atom.Writable<R, W>, value: W): void {
75
+ atom.write(this.ensureNode(atom).writeContext, value)
76
+ }
77
+
78
+ setSerializable(key: string, encoded: unknown): void {
79
+ this.preloadedSerializable.set(key, encoded)
80
+ }
81
+
82
+ modify<R, W, A>(atom: Atom.Writable<R, W>, f: (_: R) => [returnValue: A, nextValue: W]): A {
83
+ const node = this.ensureNode(atom)
84
+ const result = f(node.value())
85
+ atom.write(node.writeContext, result[1])
86
+ return result[0]
87
+ }
88
+
89
+ update<R, W>(atom: Atom.Writable<R, W>, f: (_: R) => W): void {
90
+ const node = this.ensureNode(atom)
91
+ atom.write(node.writeContext, f(node.value()))
92
+ }
93
+
94
+ refresh = <A>(atom: Atom.Atom<A>): void => {
95
+ if (atom.refresh !== undefined) {
96
+ atom.refresh(this.refresh)
97
+ } else {
98
+ this.invalidateAtom(atom)
99
+ }
100
+ }
101
+
102
+ subscribe<A>(atom: Atom.Atom<A>, f: (_: A) => void, options?: { readonly immediate?: boolean }): () => void {
103
+ const node = this.ensureNode(atom)
104
+ if (options?.immediate) {
105
+ f(node.value())
106
+ }
107
+ const remove = node.subscribe(function() {
108
+ f(node._value)
109
+ })
110
+ return () => {
111
+ remove()
112
+ if (node.canBeRemoved) {
113
+ this.scheduleNodeRemoval(node)
114
+ }
115
+ }
116
+ }
117
+
118
+ mount<A>(atom: Atom.Atom<A>) {
119
+ return this.subscribe(atom, constListener, constImmediate)
120
+ }
121
+
122
+ atomHasTtl(atom: Atom.Atom<any>): boolean {
123
+ return !atom.keepAlive && atom.idleTTL !== 0 && (atom.idleTTL !== undefined || this.defaultIdleTTL !== undefined)
124
+ }
125
+
126
+ ensureNode<A>(atom: Atom.Atom<A>): Node<A> {
127
+ const key = atomKey(atom)
128
+ let node = this.nodes.get(key)
129
+ if (node === undefined) {
130
+ node = this.createNode(atom)
131
+ this.nodes.set(key, node)
132
+ } else if (this.atomHasTtl(atom)) {
133
+ this.removeNodeTimeout(node)
134
+ }
135
+ if (typeof key === "string" && this.preloadedSerializable.has(key)) {
136
+ const encoded = this.preloadedSerializable.get(key)
137
+ this.preloadedSerializable.delete(key)
138
+ const decoded = (atom as any as Atom.Serializable)[SerializableTypeId].decode(encoded)
139
+ node.setValue(decoded)
140
+ }
141
+ return node
142
+ }
143
+
144
+ createNode<A>(atom: Atom.Atom<A>): Node<A> {
145
+ if (this.disposed) {
146
+ throw new Error(`Cannot access Atom ${atom}: registry is disposed`)
147
+ }
148
+
149
+ if (!atom.keepAlive) {
150
+ this.scheduleAtomRemoval(atom)
151
+ }
152
+ return new Node(this, atom)
153
+ }
154
+
155
+ invalidateAtom = <A>(atom: Atom.Atom<A>): void => {
156
+ this.ensureNode(atom).invalidate()
157
+ }
158
+
159
+ scheduleAtomRemoval(atom: Atom.Atom<any>): void {
160
+ this.scheduleTask(() => {
161
+ const node = this.nodes.get(atomKey(atom))
162
+ if (node !== undefined && node.canBeRemoved) {
163
+ this.removeNode(node)
164
+ }
165
+ })
166
+ }
167
+
168
+ scheduleNodeRemoval(node: Node<any>): void {
169
+ this.scheduleTask(() => {
170
+ if (node.canBeRemoved) {
171
+ this.removeNode(node)
172
+ }
173
+ })
174
+ }
175
+
176
+ removeNode(node: Node<any>): void {
177
+ if (this.atomHasTtl(node.atom)) {
178
+ this.setNodeTimeout(node)
179
+ } else {
180
+ this.nodes.delete(atomKey(node.atom))
181
+ node.remove()
182
+ }
183
+ }
184
+
185
+ setNodeTimeout(node: Node<any>): void {
186
+ if (this.nodeTimeoutBucket.has(node)) {
187
+ return
188
+ }
189
+
190
+ let idleTTL = node.atom.idleTTL ?? this.defaultIdleTTL!
191
+ if (this.#currentSweepTTL !== null) {
192
+ idleTTL -= this.#currentSweepTTL
193
+ if (idleTTL <= 0) {
194
+ this.nodes.delete(atomKey(node.atom))
195
+ node.remove()
196
+ return
197
+ }
198
+ }
199
+ const ttl = Math.ceil(idleTTL! / this.timeoutResolution) * this.timeoutResolution
200
+ const timestamp = Date.now() + ttl
201
+ const bucket = timestamp - (timestamp % this.timeoutResolution) + this.timeoutResolution
202
+
203
+ let entry = this.timeoutBuckets.get(bucket)
204
+ if (entry === undefined) {
205
+ entry = [
206
+ new Set<Node<any>>(),
207
+ setTimeout(() => this.sweepBucket(bucket), bucket - Date.now())
208
+ ]
209
+ this.timeoutBuckets.set(bucket, entry)
210
+ }
211
+ entry[0].add(node)
212
+ this.nodeTimeoutBucket.set(node, bucket)
213
+ }
214
+
215
+ removeNodeTimeout(node: Node<any>): void {
216
+ const bucket = this.nodeTimeoutBucket.get(node)
217
+ if (bucket === undefined) {
218
+ return
219
+ }
220
+ this.nodeTimeoutBucket.delete(node)
221
+ this.scheduleNodeRemoval(node)
222
+
223
+ const [nodes, handle] = this.timeoutBuckets.get(bucket)!
224
+ nodes.delete(node)
225
+ if (nodes.size === 0) {
226
+ clearTimeout(handle)
227
+ this.timeoutBuckets.delete(bucket)
228
+ }
229
+ }
230
+
231
+ #currentSweepTTL: number | null = null
232
+ sweepBucket(bucket: number): void {
233
+ const nodes = this.timeoutBuckets.get(bucket)![0]
234
+ this.timeoutBuckets.delete(bucket)
235
+
236
+ nodes.forEach((node) => {
237
+ if (!node.canBeRemoved) {
238
+ return
239
+ }
240
+ this.nodeTimeoutBucket.delete(node)
241
+ this.nodes.delete(atomKey(node.atom))
242
+ this.#currentSweepTTL = node.atom.idleTTL ?? this.defaultIdleTTL!
243
+ node.remove()
244
+ this.#currentSweepTTL = null
245
+ })
246
+ }
247
+
248
+ reset(): void {
249
+ this.timeoutBuckets.forEach(([, handle]) => clearTimeout(handle))
250
+ this.timeoutBuckets.clear()
251
+ this.nodeTimeoutBucket.clear()
252
+
253
+ this.nodes.forEach((node) => node.remove())
254
+ this.nodes.clear()
255
+ }
256
+
257
+ dispose(): void {
258
+ this.disposed = true
259
+ this.reset()
260
+ }
261
+ }
262
+
263
+ const enum NodeFlags {
264
+ alive = 1 << 0,
265
+ initialized = 1 << 1,
266
+ waitingForValue = 1 << 2
267
+ }
268
+
269
+ const enum NodeState {
270
+ uninitialized = NodeFlags.alive | NodeFlags.waitingForValue,
271
+ stale = NodeFlags.alive | NodeFlags.initialized | NodeFlags.waitingForValue,
272
+ valid = NodeFlags.alive | NodeFlags.initialized,
273
+ removed = 0
274
+ }
275
+
276
+ class Node<A> {
277
+ constructor(
278
+ readonly registry: RegistryImpl,
279
+ readonly atom: Atom.Atom<A>
280
+ ) {
281
+ this.writeContext = new WriteContextImpl(registry, this)
282
+ }
283
+
284
+ state: NodeState = NodeState.uninitialized
285
+ lifetime: Lifetime<A> | undefined
286
+ writeContext: WriteContextImpl<A>
287
+
288
+ parents: Array<Node<any>> = []
289
+ previousParents: Array<Node<any>> | undefined
290
+ children: Array<Node<any>> = []
291
+ listeners: Array<() => void> = []
292
+ skipInvalidation = false
293
+
294
+ get canBeRemoved(): boolean {
295
+ return !this.atom.keepAlive && this.listeners.length === 0 && this.children.length === 0 &&
296
+ this.state !== 0
297
+ }
298
+
299
+ _value: A = undefined as any
300
+ value(): A {
301
+ if ((this.state & NodeFlags.waitingForValue) !== 0) {
302
+ this.lifetime = makeLifetime(this)
303
+ const value = this.atom.read(this.lifetime)
304
+ if ((this.state & NodeFlags.waitingForValue) !== 0) {
305
+ this.setValue(value)
306
+ }
307
+
308
+ if (this.previousParents) {
309
+ const parents = this.previousParents
310
+ this.previousParents = undefined
311
+ for (let i = 0; i < parents.length; i++) {
312
+ parents[i].removeChild(this)
313
+ if (parents[i].canBeRemoved) {
314
+ this.registry.scheduleNodeRemoval(parents[i])
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ return this._value
321
+ }
322
+
323
+ valueOption(): Option.Option<A> {
324
+ if ((this.state & NodeFlags.initialized) === 0) {
325
+ return Option.none()
326
+ }
327
+ return Option.some(this._value)
328
+ }
329
+
330
+ setValue(value: A): void {
331
+ if ((this.state & NodeFlags.initialized) === 0) {
332
+ this.state = NodeState.valid
333
+ this._value = value
334
+
335
+ if (batchState.phase === BatchPhase.collect) {
336
+ batchState.notify.add(this)
337
+ } else {
338
+ this.notify()
339
+ }
340
+
341
+ return
342
+ }
343
+
344
+ this.state = NodeState.valid
345
+ if (Equal.equals(this._value, value)) {
346
+ return
347
+ }
348
+
349
+ this._value = value
350
+ if (this.skipInvalidation) {
351
+ this.skipInvalidation = false
352
+ } else {
353
+ this.invalidateChildren()
354
+ }
355
+
356
+ if (this.listeners.length > 0) {
357
+ if (batchState.phase === BatchPhase.collect) {
358
+ batchState.notify.add(this)
359
+ } else {
360
+ this.notify()
361
+ }
362
+ }
363
+ }
364
+
365
+ addParent(parent: Node<any>): void {
366
+ this.parents.push(parent)
367
+ if (this.previousParents !== undefined) {
368
+ const index = this.previousParents.indexOf(parent)
369
+ if (index !== -1) {
370
+ this.previousParents[index] = this.previousParents[this.previousParents.length - 1]
371
+ if (this.previousParents.pop() === undefined) {
372
+ this.previousParents = undefined
373
+ }
374
+ }
375
+ }
376
+
377
+ if (parent.children.indexOf(this) === -1) {
378
+ parent.children.push(this)
379
+ }
380
+ }
381
+
382
+ removeChild(child: Node<any>): void {
383
+ const index = this.children.indexOf(child)
384
+ if (index !== -1) {
385
+ this.children[index] = this.children[this.children.length - 1]
386
+ this.children.pop()
387
+ }
388
+ }
389
+
390
+ invalidate(): void {
391
+ if (this.state === NodeState.valid) {
392
+ this.state = NodeState.stale
393
+ this.disposeLifetime()
394
+ }
395
+
396
+ if (batchState.phase === BatchPhase.collect) {
397
+ batchState.stale.push(this)
398
+ } else if (this.atom.lazy && this.listeners.length === 0 && !childrenAreActive(this.children)) {
399
+ this.invalidateChildren()
400
+ this.skipInvalidation = true
401
+ } else {
402
+ this.value()
403
+ }
404
+ }
405
+
406
+ invalidateChildren(): void {
407
+ if (this.children.length === 0) {
408
+ return
409
+ }
410
+
411
+ const children = this.children
412
+ this.children = []
413
+ for (let i = 0; i < children.length; i++) {
414
+ children[i].invalidate()
415
+ }
416
+ }
417
+
418
+ notify(): void {
419
+ for (let i = 0; i < this.listeners.length; i++) {
420
+ this.listeners[i]()
421
+ }
422
+
423
+ if (batchState.phase === BatchPhase.commit) {
424
+ batchState.notify.delete(this)
425
+ }
426
+ }
427
+
428
+ disposeLifetime(): void {
429
+ if (this.lifetime !== undefined) {
430
+ this.lifetime.dispose()
431
+ this.lifetime = undefined
432
+ }
433
+
434
+ if (this.parents.length !== 0) {
435
+ this.previousParents = this.parents
436
+ this.parents = []
437
+ }
438
+ }
439
+
440
+ remove() {
441
+ this.state = NodeState.removed
442
+ this.listeners = []
443
+
444
+ if (this.lifetime === undefined) {
445
+ return
446
+ }
447
+
448
+ this.disposeLifetime()
449
+
450
+ if (this.previousParents === undefined) {
451
+ return
452
+ }
453
+
454
+ const parents = this.previousParents
455
+ this.previousParents = undefined
456
+ for (let i = 0; i < parents.length; i++) {
457
+ parents[i].removeChild(this)
458
+ if (parents[i].canBeRemoved) {
459
+ this.registry.removeNode(parents[i])
460
+ }
461
+ }
462
+ }
463
+
464
+ subscribe(listener: () => void): () => void {
465
+ this.listeners.push(listener)
466
+ return () => {
467
+ const index = this.listeners.indexOf(listener)
468
+ if (index !== -1) {
469
+ this.listeners[index] = this.listeners[this.listeners.length - 1]
470
+ this.listeners.pop()
471
+ }
472
+ }
473
+ }
474
+ }
475
+
476
+ function childrenAreActive(children: Array<Node<any>>): boolean {
477
+ if (children.length === 0) {
478
+ return false
479
+ }
480
+ for (let i = 0, len = children.length; i < len; i++) {
481
+ const child = children[i]
482
+ if (!child.atom.lazy || child.listeners.length > 0) {
483
+ return true
484
+ }
485
+ }
486
+ for (let i = 0, len = children.length; i < len; i++) {
487
+ if (childrenAreActive(children[i].children)) {
488
+ return true
489
+ }
490
+ }
491
+ return false
492
+ }
493
+
494
+ interface Lifetime<A> extends Atom.Context {
495
+ isFn: boolean
496
+ readonly node: Node<A>
497
+ finalizers: Array<() => void> | undefined
498
+ disposed: boolean
499
+ readonly dispose: () => void
500
+ }
501
+
502
+ const disposedError = (atom: Atom.Atom<any>): Error => new Error(`Cannot use context of disposed Atom: ${atom}`)
503
+
504
+ const LifetimeProto: Omit<Lifetime<any>, "node" | "finalizers" | "disposed" | "isFn"> = {
505
+ get registry(): RegistryImpl {
506
+ return (this as Lifetime<any>).node.registry
507
+ },
508
+
509
+ addFinalizer(this: Lifetime<any>, f: () => void): void {
510
+ if (this.disposed) {
511
+ throw disposedError(this.node.atom)
512
+ }
513
+ this.finalizers ??= []
514
+ this.finalizers.push(f)
515
+ },
516
+
517
+ get<A>(this: Lifetime<any>, atom: Atom.Atom<A>): A {
518
+ if (this.disposed) {
519
+ throw disposedError(this.node.atom)
520
+ }
521
+ const parent = this.node.registry.ensureNode(atom)
522
+ this.node.addParent(parent)
523
+ return parent.value()
524
+ },
525
+
526
+ result<A, E>(this: Lifetime<any>, atom: Atom.Atom<Result.Result<A, E>>, options?: {
527
+ readonly suspendOnWaiting?: boolean | undefined
528
+ }): Effect.Effect<A, E> {
529
+ if (this.disposed) {
530
+ throw disposedError(this.node.atom)
531
+ } else if (this.isFn) {
532
+ return this.resultOnce(atom)
533
+ }
534
+ const result = this.get(atom)
535
+ if (options?.suspendOnWaiting && result.waiting) {
536
+ return Effect.never
537
+ }
538
+ switch (result._tag) {
539
+ case "Initial": {
540
+ return Effect.never
541
+ }
542
+ case "Failure": {
543
+ return Exit.failCause(result.cause)
544
+ }
545
+ case "Success": {
546
+ return Effect.succeed(result.value)
547
+ }
548
+ }
549
+ },
550
+
551
+ resultOnce<A, E>(this: Lifetime<any>, atom: Atom.Atom<Result.Result<A, E>>, options?: {
552
+ readonly suspendOnWaiting?: boolean | undefined
553
+ }): Effect.Effect<A, E> {
554
+ if (this.disposed) {
555
+ throw disposedError(this.node.atom)
556
+ }
557
+ return Effect.async<A, E>((resume) => {
558
+ const result = this.once(atom)
559
+ if (result._tag !== "Initial" && !(options?.suspendOnWaiting && result.waiting)) {
560
+ return resume(Result.toExit(result) as any)
561
+ }
562
+ const cancel = this.node.registry.subscribe(atom, (result) => {
563
+ if (result._tag === "Initial" || (options?.suspendOnWaiting && result.waiting)) return
564
+ cancel()
565
+ resume(Result.toExit(result) as any)
566
+ }, { immediate: false })
567
+ return Effect.sync(cancel)
568
+ })
569
+ },
570
+
571
+ some<A>(this: Lifetime<any>, atom: Atom.Atom<Option.Option<A>>): Effect.Effect<A> {
572
+ if (this.disposed) {
573
+ throw disposedError(this.node.atom)
574
+ } else if (this.isFn) {
575
+ return this.someOnce(atom)
576
+ }
577
+ const result = this.get(atom)
578
+ return result._tag === "None" ? Effect.never : Effect.succeed(result.value)
579
+ },
580
+
581
+ someOnce<A>(this: Lifetime<any>, atom: Atom.Atom<Option.Option<A>>): Effect.Effect<A> {
582
+ if (this.disposed) {
583
+ throw disposedError(this.node.atom)
584
+ }
585
+ return Effect.async<A>((resume) => {
586
+ const result = this.once(atom)
587
+ if (Option.isSome(result)) {
588
+ return resume(Effect.succeed(result.value))
589
+ }
590
+ const cancel = this.node.registry.subscribe(atom, (result) => {
591
+ if (Option.isNone(result)) return
592
+ cancel()
593
+ resume(Effect.succeed(result.value))
594
+ }, { immediate: false })
595
+ return Effect.sync(cancel)
596
+ })
597
+ },
598
+
599
+ once<A>(this: Lifetime<any>, atom: Atom.Atom<A>): A {
600
+ if (this.disposed) {
601
+ throw disposedError(this.node.atom)
602
+ }
603
+ return this.node.registry.get(atom)
604
+ },
605
+
606
+ self<A>(this: Lifetime<any>): Option.Option<A> {
607
+ if (this.disposed) {
608
+ throw disposedError(this.node.atom)
609
+ }
610
+ return this.node.valueOption() as any
611
+ },
612
+
613
+ refresh<A>(this: Lifetime<any>, atom: Atom.Atom<A>): void {
614
+ if (this.disposed) {
615
+ throw disposedError(this.node.atom)
616
+ }
617
+ this.node.registry.refresh(atom)
618
+ },
619
+
620
+ refreshSelf(this: Lifetime<any>): void {
621
+ if (this.disposed) {
622
+ throw disposedError(this.node.atom)
623
+ }
624
+ this.node.invalidate()
625
+ },
626
+
627
+ mount<A>(this: Lifetime<any>, atom: Atom.Atom<A>): void {
628
+ if (this.disposed) {
629
+ throw disposedError(this.node.atom)
630
+ }
631
+ this.addFinalizer(this.node.registry.mount(atom))
632
+ },
633
+
634
+ subscribe<A>(this: Lifetime<any>, atom: Atom.Atom<A>, f: (_: A) => void, options?: {
635
+ readonly immediate?: boolean
636
+ }): void {
637
+ if (this.disposed) {
638
+ throw disposedError(this.node.atom)
639
+ }
640
+ this.addFinalizer(this.node.registry.subscribe(atom, f, options))
641
+ },
642
+
643
+ setSelf<A>(this: Lifetime<any>, a: A): void {
644
+ if (this.disposed) {
645
+ throw disposedError(this.node.atom)
646
+ }
647
+ this.node.setValue(a as any)
648
+ },
649
+
650
+ set<R, W>(this: Lifetime<any>, atom: Atom.Writable<R, W>, value: W): void {
651
+ if (this.disposed) {
652
+ throw disposedError(this.node.atom)
653
+ }
654
+ this.node.registry.set(atom, value)
655
+ },
656
+
657
+ stream<A>(this: Lifetime<any>, atom: Atom.Atom<A>, options?: {
658
+ readonly withoutInitialValue?: boolean
659
+ readonly bufferSize?: number
660
+ }) {
661
+ if (this.disposed) {
662
+ throw disposedError(this.node.atom)
663
+ }
664
+
665
+ return pipe(
666
+ Effect.acquireRelease(
667
+ Queue.bounded<A>(options?.bufferSize ?? 16),
668
+ Queue.shutdown
669
+ ),
670
+ Effect.tap((queue) =>
671
+ Effect.acquireRelease(
672
+ Effect.sync(() => {
673
+ return this.node.registry.subscribe(atom, (_) => {
674
+ Queue.unsafeOffer(queue, _)
675
+ }, { immediate: options?.withoutInitialValue !== true })
676
+ }),
677
+ (cancel) => Effect.sync(cancel)
678
+ )
679
+ ),
680
+ Effect.map((queue) => Stream.fromQueue(queue)),
681
+ Stream.unwrapScoped
682
+ )
683
+ },
684
+
685
+ streamResult<A, E>(this: Lifetime<any>, atom: Atom.Atom<Result.Result<A, E>>, options?: {
686
+ readonly withoutInitialValue?: boolean
687
+ readonly bufferSize?: number
688
+ }): Stream.Stream<A, E> {
689
+ return pipe(
690
+ this.stream(atom, options),
691
+ Stream.filter(Result.isNotInitial),
692
+ Stream.flatMap((result) =>
693
+ result._tag === "Success" ? Stream.succeed(result.value) : Stream.failCause(result.cause)
694
+ )
695
+ )
696
+ },
697
+
698
+ dispose(this: Lifetime<any>): void {
699
+ this.disposed = true
700
+ if (this.finalizers === undefined) {
701
+ return
702
+ }
703
+
704
+ const finalizers = this.finalizers
705
+ this.finalizers = undefined
706
+ for (let i = finalizers.length - 1; i >= 0; i--) {
707
+ finalizers[i]()
708
+ }
709
+ }
710
+ }
711
+
712
+ const makeLifetime = <A>(node: Node<A>): Lifetime<A> => {
713
+ function get<A>(atom: Atom.Atom<A>): A {
714
+ if (get.disposed) {
715
+ throw disposedError(atom)
716
+ } else if (get.isFn) {
717
+ return node.registry.get(atom)
718
+ }
719
+ const parent = node.registry.ensureNode(atom)
720
+ node.addParent(parent)
721
+ return parent.value()
722
+ }
723
+ Object.setPrototypeOf(get, LifetimeProto)
724
+ get.isFn = false
725
+ get.disposed = false
726
+ get.finalizers = undefined
727
+ get.node = node
728
+ return get as any
729
+ }
730
+
731
+ class WriteContextImpl<A> implements Atom.WriteContext<A> {
732
+ constructor(
733
+ readonly registry: RegistryImpl,
734
+ readonly node: Node<A>
735
+ ) {}
736
+ get<A>(atom: Atom.Atom<A>): A {
737
+ return this.registry.get(atom)
738
+ }
739
+ set<R, W>(atom: Atom.Writable<R, W>, value: W) {
740
+ return this.registry.set(atom, value)
741
+ }
742
+ setSelf(value: any) {
743
+ return this.node.setValue(value)
744
+ }
745
+ refreshSelf() {
746
+ return this.node.invalidate()
747
+ }
748
+ }
749
+
750
+ // -----------------------------------------------------------------------------
751
+ // batching
752
+ // -----------------------------------------------------------------------------
753
+
754
+ /** @internal */
755
+ export const enum BatchPhase {
756
+ disabled,
757
+ collect,
758
+ commit
759
+ }
760
+
761
+ /** @internal */
762
+ export const batchState = globalValue("@effect-atom/atom/Registry/batchState", () => ({
763
+ phase: BatchPhase.disabled,
764
+ depth: 0,
765
+ stale: [] as Array<Node<any>>,
766
+ notify: new Set<Node<any>>()
767
+ }))
768
+
769
+ /** @internal */
770
+ export function batch(f: () => void): void {
771
+ batchState.phase = BatchPhase.collect
772
+ batchState.depth++
773
+ try {
774
+ f()
775
+ if (batchState.depth === 1) {
776
+ for (let i = 0; i < batchState.stale.length; i++) {
777
+ batchRebuildNode(batchState.stale[i])
778
+ }
779
+ batchState.phase = BatchPhase.commit
780
+ for (const node of batchState.notify) {
781
+ node.notify()
782
+ }
783
+ batchState.notify.clear()
784
+ }
785
+ } finally {
786
+ batchState.depth--
787
+ if (batchState.depth === 0) {
788
+ batchState.phase = BatchPhase.disabled
789
+ batchState.stale = []
790
+ }
791
+ }
792
+ }
793
+
794
+ function batchRebuildNode(node: Node<any>) {
795
+ if (node.state === NodeState.valid) {
796
+ return
797
+ }
798
+
799
+ for (let i = 0; i < node.parents.length; i++) {
800
+ const parent = node.parents[i]
801
+ if (parent.state !== NodeState.valid) {
802
+ batchRebuildNode(parent)
803
+ }
804
+ }
805
+
806
+ // @ts-ignore
807
+ if (node.state !== NodeState.valid) {
808
+ node.value()
809
+ }
810
+ }