@xmachines/play-atom 5.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @xmachines/play-atom
2
+
3
+ The atom primitives for XMachines. They give the Play Architecture fine-grained reactive state. The primitives propagate state without a glitch and without a subscription of the consumer.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-5.0.0-blue)](https://www.npmjs.com/package/@xmachines/play-atom)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @xmachines/play-atom @xmachines/play @xstate/store
11
+ ```
12
+
13
+ **Peer dependencies.** Install them with the package:
14
+
15
+ - [`@xmachines/play`](../play/README.md) — the core protocol. This package reads `asCleanup` and `Cleanup` from it.
16
+ - `@xstate/store` `^4.2.3` — the atom engine.
17
+
18
+ Install ONE copy of `@xstate/store` — see [One instance](#one-instance).
19
+
20
+ ## Overview
21
+
22
+ This package DECLARES the reactive contract of the Play Architecture, and it binds an engine to that contract in one module. The engine of today is [`@xstate/store`](https://stately.ai/docs/xstate-store), whose reactive core adapts the [Alien Signals](https://github.com/stackblitz/alien-signals) algorithm. It adds the `watchAtom` utility, which owns the subscription lifecycle, and `createWritableAtom`, which removes an overload hazard for a generic value type.
23
+
24
+ The types belong to THIS package. `Atom`, `Readable` and each name beside them are declarations here, and no forward of a type of the engine. Therefore the published `.d.ts` of a consumer names the engine never, and a change of the engine reaches this package alone.
25
+
26
+ **Import every atom in the XMachines ecosystem from this package**, and not from `@xstate/store`. One import point keeps each update and each API change in one place.
27
+
28
+ ## Usage
29
+
30
+ ### `createAtom(value)` — writable reactive state
31
+
32
+ ```typescript
33
+ import { createAtom } from "@xmachines/play-atom";
34
+
35
+ const count = createAtom(0);
36
+
37
+ console.log(count.get()); // 0
38
+ count.set(5);
39
+ count.set((previous) => previous + 1);
40
+ console.log(count.get()); // 6
41
+ ```
42
+
43
+ ### `createAtom(fn)` — memoized derived values
44
+
45
+ ```typescript
46
+ import { createAtom } from "@xmachines/play-atom";
47
+
48
+ const count = createAtom(0);
49
+ const doubled = createAtom(() => count.get() * 2);
50
+
51
+ console.log(doubled.get()); // 0 (computed on first access)
52
+ count.set(5);
53
+ console.log(doubled.get()); // 10 (recomputed because dependency changed)
54
+ console.log(doubled.get()); // 10 (memoized — no recomputation)
55
+ ```
56
+
57
+ A computation tracks each atom that it reads. A dynamic branch is safe: the computation keeps only the atoms of the _current_ execution path as its dependencies.
58
+
59
+ The getter takes ONE parameter: the value that the atom holds already. It is `undefined` before the first derivation. Return it to keep the atom still, which stops the propagation at this atom:
60
+
61
+ ```typescript
62
+ const view = createAtom((previous) => {
63
+ const next = derive(source.get());
64
+ return equivalent(previous, next) ? (previous ?? null) : next;
65
+ });
66
+ ```
67
+
68
+ > A computed atom that something SUBSCRIBES to is eager. `@xstate/store` builds a subscription on an effect that reads the atom, so a write to a dependency evaluates the derivation at once, inside that write. A computed atom with no subscriber stays lazy.
69
+
70
+ ### `createWritableAtom(value)` — the writable atom, with one signature
71
+
72
+ `createAtom` carries two signatures, and TypeScript must prove the argument is not a function before it reaches the writable one. For a concrete type it proves that at once. For a type parameter, or a conditional type over one, it cannot, so it picks the computed signature and returns a `ReadonlyAtom` that carries no `set`.
73
+
74
+ ```typescript
75
+ import { createWritableAtom, type Atom } from "@xmachines/play-atom";
76
+
77
+ class Holder<T extends { value: unknown }> {
78
+ public readonly state: Atom<T["value"]>;
79
+
80
+ constructor(seed: T["value"]) {
81
+ // createAtom(seed) would give a ReadonlyAtom here.
82
+ this.state = createWritableAtom(seed);
83
+ }
84
+ }
85
+ ```
86
+
87
+ Use `createAtom` everywhere else.
88
+
89
+ ### `watchAtom` — the coalesced subscription
90
+
91
+ Use `watchAtom` to subscribe to an `Atom` or to a `ReadonlyAtom`. The callback receives the value after each change. `watchAtom` groups the updates of one synchronous batch into a single microtask.
92
+
93
+ ```typescript
94
+ import { createAtom, watchAtom } from "@xmachines/play-atom";
95
+
96
+ const count = createAtom(0);
97
+
98
+ const cleanup = watchAtom(count, (value) => {
99
+ console.log("count changed:", value);
100
+ });
101
+
102
+ count.set(1); // coalesced with any rapid synchronous changes
103
+ count.set(2);
104
+ count.set(3); // → logs "count changed: 3" once, from a microtask
105
+
106
+ // Stop watching
107
+ cleanup();
108
+ ```
109
+
110
+ The cleanup function is idempotent. A second call is safe, and it does not throw. It is also a `Disposable`, so `using cleanup = watchAtom(...)` releases it at the end of the scope.
111
+
112
+ ### `atom.subscribe` — synchronous observation
113
+
114
+ Every atom carries `subscribe`. The observer runs SYNCHRONOUSLY, inside the `set` of the atom that changed, and nothing coalesces the calls. An observer that throws therefore throws inside that `set`, and the failure lands in the code that wrote the atom. Prefer `watchAtom` unless you need the notification at the exact moment of the write.
115
+
116
+ ```typescript
117
+ const subscription = count.subscribe((value) => console.log(value));
118
+ subscription.unsubscribe();
119
+ ```
120
+
121
+ ### Custom equality
122
+
123
+ `createAtom` accepts a `compare` option. It controls when an atom notifies its dependents. The default is `Object.is`.
124
+
125
+ ```typescript
126
+ import { createAtom } from "@xmachines/play-atom";
127
+ import type { AtomOptions } from "@xmachines/play-atom";
128
+
129
+ const options: AtomOptions<{ name: string; age: number }> = {
130
+ compare: (a, b) => a.name === b.name && a.age === b.age,
131
+ };
132
+
133
+ const person = createAtom({ name: "Alice", age: 30 }, options);
134
+ // A structurally identical value does not notify the dependents
135
+ person.set({ name: "Alice", age: 30 });
136
+ ```
137
+
138
+ ## One instance
139
+
140
+ `@xstate/store` holds the active subscriber in a variable of its own module. Two copies in one application build two separate graphs: a computed atom of the first copy and a subscription of the second copy never meet, the subscriber stays silent, and no error reports the fault. Every propagation of state in this architecture goes through an atom, so the second copy stops the whole reactivity of an application.
141
+
142
+ This package therefore declares `@xstate/store` as a peer dependency. A renderer declares it no more: a renderer imports the engine in no source file, and it reads the atom through this package. Bring one copy, and bring it yourself.
143
+
144
+ ## API Summary
145
+
146
+ | Export | Kind | Description |
147
+ | ---------------------------------- | --------- | --------------------------------------------------------------------------------------------- |
148
+ | `createAtom(value, options?)` | function | The writable atom |
149
+ | `createAtom(fn, options?)` | function | The computed atom |
150
+ | `createWritableAtom(value, opts?)` | function | The writable atom, with one signature, for a generic value type |
151
+ | `createAsyncAtom(fn, options?)` | function | The computed atom of a promise. It holds an `AsyncAtomState`. `fn` reads an `AbortSignal` |
152
+ | `watchAtom(atom, onValue)` | function | The coalesced subscription helper. It returns a `Cleanup` |
153
+ | `Atom<T>` | interface | The writable atom (`.get()`, `.set()`, `.subscribe()`) |
154
+ | `ReadonlyAtom<T>` | interface | The computed atom (`.get()`, `.subscribe()`) |
155
+ | `Readable<T>` | interface | The read side of either one (`.get()`, `.subscribe()`) |
156
+ | `BaseAtom<T>` | interface | The common base of both kinds |
157
+ | `AnyAtom` | type | An atom of an unknown value type |
158
+ | `AtomOptions<T>` | interface | The options object of `createAtom` (`compare?`) |
159
+ | `AtomObserver<T>` | interface | The observer of `subscribe`, in the object form |
160
+ | `AtomSubscription` | interface | The release that `subscribe` returns |
161
+ | `AsyncAtomState<TData>` | type | The state of `createAsyncAtom`: `pending`, `done`, or `error` |
162
+ | `AsyncAtomOptions` | interface | What `createAsyncAtom` hands its getter: the `signal` that aborts a stale run |
163
+ | `CreateAtom` | interface | The two call signatures of `createAtom` |
164
+ | `Cleanup` | type | The release that `watchAtom` returns, re-exported from [`@xmachines/play`](../play/README.md) |
165
+
166
+ The observer types carry the `Atom` prefix on purpose: `xstate` declares `Observer` and `Subscription` too, so a plain name here would put two declarations of one name in one file. `Subscribable` stays out of the surface for the same reason.
167
+
168
+ ## Migrating from `@xmachines/play-signals`
169
+
170
+ This package replaces `@xmachines/play-signals`, which wrapped the TC39 Signals polyfill. A codemod rewrites the call sites:
171
+
172
+ ```bash
173
+ curl -O https://gitlab.com/xmachin-es/xmachines-js/-/raw/main/scripts/codemod-play-atom.mjs
174
+ node codemod-play-atom.mjs --dry-run .
175
+ ```
176
+
177
+ See [Migrating to @xmachines/play-atom](../docs/contributing/migrating-to-play-atom.md) for the name table and the three behaviour changes that no codemod can make for you.
178
+
179
+ ## Requirements
180
+
181
+ - **Node.js** `>= 24.0.0`
182
+ - **TypeScript** `5.7+` (for a consumer that uses TypeScript)
183
+ - **`@xstate/store`** `^4.2.3` — the atom engine, and a PEER dependency
184
+
185
+ XMachines 5.0.0 moved the engine from `@xstate/store` 3 to 4. A computed getter took a `read` helper in its first position there, and it takes the previous value alone now. Delete that first parameter:
186
+
187
+ ```diff
188
+ -const derived = createAtom((_read, previous) => …);
189
+ +const derived = createAtom((previous) => …);
190
+ ```
191
+
192
+ ## License
193
+
194
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,45 @@
1
+ import type { AsyncAtomOptions, AsyncAtomState, Atom, AtomOptions, ReadonlyAtom } from "./types.js";
2
+ /**
3
+ * The two forms of `createAtom`
4
+ *
5
+ * A function argument builds a COMPUTED atom, which reads its dependencies and gives a
6
+ * `ReadonlyAtom`. Any other argument builds a WRITABLE atom, which holds the value and
7
+ * gives an `Atom`.
8
+ *
9
+ * A computed getter reads each dependency with the `get` of that dependency, and the
10
+ * engine records the edge as it reads. Its one parameter is the PREVIOUS value, which is
11
+ * `undefined` on the first derivation. `@xstate/store` 3 passed a `read` helper in that
12
+ * first position and the previous value in the second; version 4 dropped the helper,
13
+ * because `get` tracked the edge already.
14
+ */
15
+ export interface CreateAtom {
16
+ <T>(getValue: (previous?: T) => T, options?: AtomOptions<T>): ReadonlyAtom<T>;
17
+ <T>(initialValue: T, options?: AtomOptions<T>): Atom<T>;
18
+ }
19
+ /**
20
+ * Builds a writable atom from a value, and a computed atom from a function
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const count = createAtom(0);
25
+ * const doubled = createAtom(() => count.get() * 2);
26
+ * ```
27
+ */
28
+ export declare const createAtom: CreateAtom;
29
+ /**
30
+ * Builds a read-only atom that holds the state of a promise
31
+ *
32
+ * The atom starts at `pending`, and it moves to `done` or to `error`.
33
+ *
34
+ * The getter receives an `AbortSignal`. The engine aborts it when the atom derives again
35
+ * before the run settles, so a stale answer reaches the atom never. A getter that needs no
36
+ * signal takes no parameter.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const user = createAsyncAtom(() => fetch("/me").then((response) => response.json()));
41
+ * if (user.get().status === "done") console.log("ready");
42
+ * ```
43
+ */
44
+ export declare const createAsyncAtom: <T>(getValue: (options: AsyncAtomOptions) => Promise<T>, options?: AtomOptions<AsyncAtomState<T>>) => ReadonlyAtom<AsyncAtomState<T>>;
45
+ //# sourceMappingURL=create-atom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-atom.d.ts","sourceRoot":"","sources":["../src/create-atom.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpG;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IAC1B,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;CACxD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,UAA6B,CAAC;AAEvD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,EAAE,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,EACnD,OAAO,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,KACpC,YAAY,CAAC,cAAc,CAAC,CAAC,CAAC,CAAyB,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The constructors of an atom, bound to the vocabulary of this package
3
+ *
4
+ * The module imports the engine, and it gives back the SAME functions under the types
5
+ * of `./types.js`. It adds no wrapper at runtime: a binding of a constant costs
6
+ * nothing, and the identity of the function stays the identity of the engine.
7
+ *
8
+ * The declaration is what matters. Without it, `createAtom` carries the return type of
9
+ * the engine, and every `.d.ts` that holds an atom names `@xstate/store`. A consumer
10
+ * then needs that manifest entry to compile, and a change of the engine becomes a
11
+ * breaking change of nineteen packages. With it, the name of the engine stops here.
12
+ */
13
+ import { createAsyncAtom as createEngineAsyncAtom, createAtom as createEngineAtom, } from "@xstate/store";
14
+ /**
15
+ * Builds a writable atom from a value, and a computed atom from a function
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const count = createAtom(0);
20
+ * const doubled = createAtom(() => count.get() * 2);
21
+ * ```
22
+ */
23
+ export const createAtom = createEngineAtom;
24
+ /**
25
+ * Builds a read-only atom that holds the state of a promise
26
+ *
27
+ * The atom starts at `pending`, and it moves to `done` or to `error`.
28
+ *
29
+ * The getter receives an `AbortSignal`. The engine aborts it when the atom derives again
30
+ * before the run settles, so a stale answer reaches the atom never. A getter that needs no
31
+ * signal takes no parameter.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const user = createAsyncAtom(() => fetch("/me").then((response) => response.json()));
36
+ * if (user.get().status === "done") console.log("ready");
37
+ * ```
38
+ */
39
+ export const createAsyncAtom = createEngineAsyncAtom;
40
+ //# sourceMappingURL=create-atom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-atom.js","sourceRoot":"","sources":["../src/create-atom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACN,eAAe,IAAI,qBAAqB,EACxC,UAAU,IAAI,gBAAgB,GAC9B,MAAM,eAAe,CAAC;AAsBvB;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,UAAU,GAAe,gBAAgB,CAAC;AAEvD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,eAAe,GAGW,qBAAqB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { Atom, AtomOptions } from "./types.js";
2
+ /**
3
+ * Builds a WRITABLE atom, and never a computed one.
4
+ *
5
+ * `createAtom` carries two signatures: a function argument builds a computed atom, and
6
+ * a value argument builds a writable one. TypeScript reads the signatures in order, so
7
+ * it needs to prove that the argument is NOT a function before it reaches the second
8
+ * one. For a concrete type it proves that at once, and `createAtom` is then the right
9
+ * call.
10
+ *
11
+ * For an UNRESOLVED type the proof fails. `ReturnType<TMachine["transition"]>` inside a
12
+ * generic class is a conditional type that TypeScript cannot evaluate yet, so it cannot
13
+ * rule a function out, it picks the first signature, and the call returns a
14
+ * `ReadonlyAtom` that carries no `set`. The error arrives at the assignment, and it
15
+ * names a type that the reader never wrote.
16
+ *
17
+ * This function states the one signature. The assignment below checks that the
18
+ * signature belongs to `createAtom`, so the two stay in step and no cast hides a
19
+ * mistake.
20
+ *
21
+ * Use `createAtom` everywhere else. Reach for this function when the value type is a
22
+ * type parameter, or a conditional type over one.
23
+ *
24
+ * A CAUTION: the signature states ONE overload, and the implementation is `createAtom`
25
+ * itself, which reads a FUNCTION argument as a computed atom. A `T` that a function
26
+ * satisfies therefore gets a `ReadonlyAtom` at run time while the type says `Atom`, and
27
+ * the first `set` call throws. Hold a function in a wrapper — `createWritableAtom({ fn })`
28
+ * — as the value types of this workspace already do.
29
+ *
30
+ * @param initialValue - The first value of the atom. It must not be a function.
31
+ * @param options - The optional `compare` function. The default is `Object.is`.
32
+ * @returns The writable atom.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { createWritableAtom, type Atom } from "@xmachines/play-atom";
37
+ *
38
+ * class Holder<T extends { value: unknown }> {
39
+ * // `createAtom(seed)` would give a ReadonlyAtom<T["value"]> here.
40
+ * public readonly state: Atom<T["value"]>;
41
+ *
42
+ * constructor(seed: T["value"]) {
43
+ * this.state = createWritableAtom(seed);
44
+ * }
45
+ * }
46
+ * ```
47
+ */
48
+ export declare const createWritableAtom: <T>(initialValue: T, options?: AtomOptions<T>) => Atom<T>;
49
+ //# sourceMappingURL=create-writable-atom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-writable-atom.d.ts","sourceRoot":"","sources":["../src/create-writable-atom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,kBAAkB,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAC9E,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { createAtom } from "./create-atom.js";
2
+ /**
3
+ * Builds a WRITABLE atom, and never a computed one.
4
+ *
5
+ * `createAtom` carries two signatures: a function argument builds a computed atom, and
6
+ * a value argument builds a writable one. TypeScript reads the signatures in order, so
7
+ * it needs to prove that the argument is NOT a function before it reaches the second
8
+ * one. For a concrete type it proves that at once, and `createAtom` is then the right
9
+ * call.
10
+ *
11
+ * For an UNRESOLVED type the proof fails. `ReturnType<TMachine["transition"]>` inside a
12
+ * generic class is a conditional type that TypeScript cannot evaluate yet, so it cannot
13
+ * rule a function out, it picks the first signature, and the call returns a
14
+ * `ReadonlyAtom` that carries no `set`. The error arrives at the assignment, and it
15
+ * names a type that the reader never wrote.
16
+ *
17
+ * This function states the one signature. The assignment below checks that the
18
+ * signature belongs to `createAtom`, so the two stay in step and no cast hides a
19
+ * mistake.
20
+ *
21
+ * Use `createAtom` everywhere else. Reach for this function when the value type is a
22
+ * type parameter, or a conditional type over one.
23
+ *
24
+ * A CAUTION: the signature states ONE overload, and the implementation is `createAtom`
25
+ * itself, which reads a FUNCTION argument as a computed atom. A `T` that a function
26
+ * satisfies therefore gets a `ReadonlyAtom` at run time while the type says `Atom`, and
27
+ * the first `set` call throws. Hold a function in a wrapper — `createWritableAtom({ fn })`
28
+ * — as the value types of this workspace already do.
29
+ *
30
+ * @param initialValue - The first value of the atom. It must not be a function.
31
+ * @param options - The optional `compare` function. The default is `Object.is`.
32
+ * @returns The writable atom.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { createWritableAtom, type Atom } from "@xmachines/play-atom";
37
+ *
38
+ * class Holder<T extends { value: unknown }> {
39
+ * // `createAtom(seed)` would give a ReadonlyAtom<T["value"]> here.
40
+ * public readonly state: Atom<T["value"]>;
41
+ *
42
+ * constructor(seed: T["value"]) {
43
+ * this.state = createWritableAtom(seed);
44
+ * }
45
+ * }
46
+ * ```
47
+ */
48
+ export const createWritableAtom = createAtom;
49
+ //# sourceMappingURL=create-writable-atom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-writable-atom.js","sourceRoot":"","sources":["../src/create-writable-atom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAC9B,UAAU,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The atom primitives of the XMachines Play Architecture
3
+ *
4
+ * This package gives you the fine-grained reactive state primitives that carry every
5
+ * value across a boundary of the Play Architecture. It keeps the reactive primitive in
6
+ * one place, and it therefore protects the code from a change of the underlying
7
+ * library.
8
+ *
9
+ * **Architectural context:** the package implements **Atom-Only Reactivity (INV-03)**.
10
+ * It gives the reactive primitives that carry the communication from the Actor to the
11
+ * infrastructure, without a subscription of the consumer and without an event emitter.
12
+ * Every propagation of state in the Play Architecture uses an atom, which tracks each
13
+ * dependency and updates without a glitch.
14
+ *
15
+ * @packageDocumentation
16
+ * @module @xmachines/play-atom
17
+ *
18
+ * @example
19
+ * The basic use of an atom
20
+ * ```typescript
21
+ * import { createAtom, watchAtom } from "@xmachines/play-atom";
22
+ *
23
+ * // Create a writable atom
24
+ * const count = createAtom(0);
25
+ *
26
+ * // Create a computed atom
27
+ * const doubled = createAtom(() => count.get() * 2);
28
+ *
29
+ * // Observe the changes
30
+ * const stop = watchAtom(doubled, (value) => {
31
+ * console.log("Count:", count.get(), "Doubled:", value);
32
+ * });
33
+ *
34
+ * count.set(5); // Logs: Count: 5 Doubled: 10
35
+ * ```
36
+ *
37
+ * @see [Play RFC](../../docs/rfc/play.md) - invariant INV-03
38
+ * @see {@link https://stately.ai/docs/xstate-store | XState Store}
39
+ *
40
+ * @remarks
41
+ * **The engine:** the atom comes from `@xstate/store`, whose reactive core adapts the
42
+ * Alien Signals algorithm. The algorithm resolves the diamond problem, it recomputes no
43
+ * value that no dependency changed, and it accepts an equality function of your own
44
+ * through the `compare` option.
45
+ *
46
+ * **The reason for the separation:** this dedicated package DECLARES the contract, and
47
+ * it binds the engine to that contract in one module. Therefore one place holds each
48
+ * new version, each change of the API, and the choice of the engine itself. A consumer
49
+ * names the engine in no import and in no manifest, so a change of the engine reaches
50
+ * this package alone.
51
+ *
52
+ * **`@xstate/store` is a PEER dependency, and it must resolve to ONE instance.** The
53
+ * library holds the active subscriber in a module variable. Two copies in one
54
+ * application track no dependency across a package boundary, and a computed atom then
55
+ * updates never.
56
+ */
57
+ export { createAsyncAtom, createAtom } from "./create-atom.js";
58
+ export { createWritableAtom } from "./create-writable-atom.js";
59
+ export { watchAtom } from "./watch-atom.js";
60
+ export type { CreateAtom } from "./create-atom.js";
61
+ export type { AnyAtom, AsyncAtomOptions, AsyncAtomState, Atom, AtomObserver, AtomOptions, AtomSubscription, BaseAtom, Readable, ReadonlyAtom, } from "./types.js";
62
+ export { type Cleanup } from "@xmachines/play";
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAGH,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EACX,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,YAAY,GACZ,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The atom primitives of the XMachines Play Architecture
3
+ *
4
+ * This package gives you the fine-grained reactive state primitives that carry every
5
+ * value across a boundary of the Play Architecture. It keeps the reactive primitive in
6
+ * one place, and it therefore protects the code from a change of the underlying
7
+ * library.
8
+ *
9
+ * **Architectural context:** the package implements **Atom-Only Reactivity (INV-03)**.
10
+ * It gives the reactive primitives that carry the communication from the Actor to the
11
+ * infrastructure, without a subscription of the consumer and without an event emitter.
12
+ * Every propagation of state in the Play Architecture uses an atom, which tracks each
13
+ * dependency and updates without a glitch.
14
+ *
15
+ * @packageDocumentation
16
+ * @module @xmachines/play-atom
17
+ *
18
+ * @example
19
+ * The basic use of an atom
20
+ * ```typescript
21
+ * import { createAtom, watchAtom } from "@xmachines/play-atom";
22
+ *
23
+ * // Create a writable atom
24
+ * const count = createAtom(0);
25
+ *
26
+ * // Create a computed atom
27
+ * const doubled = createAtom(() => count.get() * 2);
28
+ *
29
+ * // Observe the changes
30
+ * const stop = watchAtom(doubled, (value) => {
31
+ * console.log("Count:", count.get(), "Doubled:", value);
32
+ * });
33
+ *
34
+ * count.set(5); // Logs: Count: 5 Doubled: 10
35
+ * ```
36
+ *
37
+ * @see [Play RFC](../../docs/rfc/play.md) - invariant INV-03
38
+ * @see {@link https://stately.ai/docs/xstate-store | XState Store}
39
+ *
40
+ * @remarks
41
+ * **The engine:** the atom comes from `@xstate/store`, whose reactive core adapts the
42
+ * Alien Signals algorithm. The algorithm resolves the diamond problem, it recomputes no
43
+ * value that no dependency changed, and it accepts an equality function of your own
44
+ * through the `compare` option.
45
+ *
46
+ * **The reason for the separation:** this dedicated package DECLARES the contract, and
47
+ * it binds the engine to that contract in one module. Therefore one place holds each
48
+ * new version, each change of the API, and the choice of the engine itself. A consumer
49
+ * names the engine in no import and in no manifest, so a change of the engine reaches
50
+ * this package alone.
51
+ *
52
+ * **`@xstate/store` is a PEER dependency, and it must resolve to ONE instance.** The
53
+ * library holds the active subscriber in a module variable. Two copies in one
54
+ * application track no dependency across a package boundary, and a computed atom then
55
+ * updates never.
56
+ */
57
+ // The atom constructors, under the types that this package declares
58
+ export { createAsyncAtom, createAtom } from "./create-atom.js";
59
+ export { createWritableAtom } from "./create-writable-atom.js";
60
+ export { watchAtom } from "./watch-atom.js";
61
+ // The release protocol of @xmachines/play. This package's own published `.d.ts` names
62
+ // exactly these, so a consumer reads them from here and needs no second manifest
63
+ // entry. It names `asCleanup` nowhere: a consumer of this package RECEIVES a
64
+ // release, and builds one only with @xmachines/play itself.
65
+ export {} from "@xmachines/play";
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,oEAAoE;AACpE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAiB5C,sFAAsF;AACtF,iFAAiF;AACjF,6EAA6E;AAC7E,4DAA4D;AAC5D,OAAO,EAAgB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The type definitions of the atom API
3
+ *
4
+ * This module DECLARES the vocabulary, and it re-exports the vocabulary of no other
5
+ * library. A consumer therefore reads the whole reactive contract of the Play
6
+ * Architecture from `@xmachines/play-atom`, and the published `.d.ts` of a consumer
7
+ * names the engine never.
8
+ *
9
+ * The declarations are structural, and they say the minimum that this architecture
10
+ * uses: `get`, `subscribe`, and `set`. The atom of `@xstate/store` satisfies each one,
11
+ * so `createAtom` returns a value that fits with no cast. A second engine that gives
12
+ * the same three members fits in the same way, and the seam therefore holds the choice
13
+ * of the engine inside this package.
14
+ *
15
+ * @see {@link https://stately.ai/docs/xstate-store | XState Store} - the engine of today
16
+ */
17
+ /**
18
+ * The release of one subscription
19
+ *
20
+ * `subscribe` returns it. Call `unsubscribe` one time. A second call does nothing.
21
+ */
22
+ export interface AtomSubscription {
23
+ unsubscribe(): void;
24
+ }
25
+ /**
26
+ * The observer of an atom, in the object form
27
+ *
28
+ * Each member is optional, so an observer of the value alone gives `next` alone. An
29
+ * atom completes never and it fails never, so `error` and `complete` stay for the
30
+ * consumers that adapt an atom to an observable.
31
+ */
32
+ export interface AtomObserver<T> {
33
+ next?: (value: T) => void;
34
+ error?: (error: unknown) => void;
35
+ complete?: () => void;
36
+ }
37
+ /**
38
+ * The read side of an atom
39
+ *
40
+ * `Readable` holds `get()` and `subscribe()`. A function that only READS an atom takes
41
+ * this type, and it therefore accepts an `Atom`, a `ReadonlyAtom`, and a store
42
+ * selection alike.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * import { createAtom, type Readable } from "@xmachines/play-atom";
47
+ *
48
+ * function show(source: Readable<string>): string {
49
+ * return source.get();
50
+ * }
51
+ *
52
+ * show(createAtom("ready")); // "ready"
53
+ * ```
54
+ */
55
+ export interface Readable<T> {
56
+ get(): T;
57
+ subscribe(observer: AtomObserver<T>): AtomSubscription;
58
+ subscribe(next: (value: T) => void, error?: (error: unknown) => void, complete?: () => void): AtomSubscription;
59
+ }
60
+ /**
61
+ * The common base of a writable atom and a computed atom
62
+ *
63
+ * Use it for a value that a caller reads and observes, when the caller must accept
64
+ * both kinds.
65
+ */
66
+ export interface BaseAtom<T> extends Readable<T> {
67
+ }
68
+ /**
69
+ * The computed atom. It evaluates late, and it memoizes the result
70
+ *
71
+ * `createAtom` returns this type for a function argument. The atom tracks each
72
+ * dependency when it runs the function, and it runs the function again only after a
73
+ * dependency changes. The atom therefore tracks each dependency for you, and you
74
+ * manage no subscription.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { createAtom } from "@xmachines/play-atom";
79
+ *
80
+ * const count = createAtom(0);
81
+ * const doubled = createAtom(() => count.get() * 2);
82
+ *
83
+ * console.log(doubled.get()); // 0
84
+ * count.set(5);
85
+ * console.log(doubled.get()); // 10 (the atom computed it again)
86
+ * console.log(doubled.get()); // 10 (from the memory, with no new computation)
87
+ * ```
88
+ */
89
+ export interface ReadonlyAtom<T> extends BaseAtom<T> {
90
+ }
91
+ /**
92
+ * The writable atom. It holds one reactive value
93
+ *
94
+ * A `get()` call inside a computed atom or inside a subscription tracks the atom as a
95
+ * dependency. A `set()` call notifies every computation and every observer that
96
+ * depends on the atom. `set` accepts a value, and it accepts a function of the
97
+ * previous value.
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * import { createAtom } from "@xmachines/play-atom";
102
+ *
103
+ * const name = createAtom("Alice");
104
+ * console.log(name.get()); // "Alice"
105
+ * name.set("Bob");
106
+ * name.set((previous) => previous.toUpperCase()); // "BOB"
107
+ * ```
108
+ */
109
+ export interface Atom<T> extends BaseAtom<T> {
110
+ /** Sets the value of the atom from the previous value. */
111
+ set(fn: (previous: T) => T): void;
112
+ /** Sets the value of the atom. */
113
+ set(value: T): void;
114
+ }
115
+ /**
116
+ * An atom of an unknown value type
117
+ *
118
+ * Use it for a collection that holds atoms of mixed types.
119
+ */
120
+ export type AnyAtom = BaseAtom<unknown>;
121
+ /**
122
+ * The options of `createAtom`
123
+ *
124
+ * `compare` decides if the value changed. The default is `Object.is`. An atom that
125
+ * compares equal notifies no observer.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * import { createAtom } from "@xmachines/play-atom";
130
+ *
131
+ * const point = createAtom(
132
+ * { x: 0, y: 0 },
133
+ * { compare: (previous, next) => previous.x === next.x && previous.y === next.y },
134
+ * );
135
+ * ```
136
+ */
137
+ export interface AtomOptions<T> {
138
+ compare?: (previous: T, next: T) => boolean;
139
+ }
140
+ /**
141
+ * The state of an atom that reads a promise
142
+ *
143
+ * `createAsyncAtom` holds this shape. Read `status` first, and the compiler then gives
144
+ * you `data` or `error`.
145
+ */
146
+ export type AsyncAtomState<TData, TError = unknown> = {
147
+ status: "pending";
148
+ } | {
149
+ status: "done";
150
+ data: TData;
151
+ } | {
152
+ status: "error";
153
+ error: TError;
154
+ };
155
+ /**
156
+ * What `createAsyncAtom` gives its getter
157
+ *
158
+ * The engine aborts the signal when the atom derives again before the run settles, so a
159
+ * stale answer reaches the atom never. Pass it to `fetch`, and the request stops with it.
160
+ */
161
+ export interface AsyncAtomOptions {
162
+ /** Aborted when the atom derives again before this run settles. */
163
+ readonly signal: AbortSignal;
164
+ }
165
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAChC,WAAW,IAAI,IAAI,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IAC1B,GAAG,IAAI,CAAC,CAAC;IACT,SAAS,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC;IACvD,SAAS,CACR,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EACxB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,EAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,GACnB,gBAAgB,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC,CAAE,SAAQ,QAAQ,CAAC,CAAC,CAAC;CAAG;AAEnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,CAAE,SAAQ,QAAQ,CAAC,CAAC,CAAC;CAAG;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC,CAAE,SAAQ,QAAQ,CAAC,CAAC,CAAC;IAC3C,0DAA0D;IAC1D,GAAG,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAClC,kCAAkC;IAClC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;AAExC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;CAC5C;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,IAC/C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC7B"}
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The type definitions of the atom API
3
+ *
4
+ * This module DECLARES the vocabulary, and it re-exports the vocabulary of no other
5
+ * library. A consumer therefore reads the whole reactive contract of the Play
6
+ * Architecture from `@xmachines/play-atom`, and the published `.d.ts` of a consumer
7
+ * names the engine never.
8
+ *
9
+ * The declarations are structural, and they say the minimum that this architecture
10
+ * uses: `get`, `subscribe`, and `set`. The atom of `@xstate/store` satisfies each one,
11
+ * so `createAtom` returns a value that fits with no cast. A second engine that gives
12
+ * the same three members fits in the same way, and the seam therefore holds the choice
13
+ * of the engine inside this package.
14
+ *
15
+ * @see {@link https://stately.ai/docs/xstate-store | XState Store} - the engine of today
16
+ */
17
+ export {};
18
+ // `Observer`, `Subscribable` and `Subscription` carry no declaration here, and the two
19
+ // names above carry the `Atom` prefix for the same reason. The three names are common,
20
+ // and `@xstate/store` is not the only library that declares them: `xstate` declares
21
+ // `Observer` and `Subscription` too, and `packages/play-xstate/src/player-actor.ts`
22
+ // imports both. A plain name here therefore puts two declarations of one name in one
23
+ // file.
24
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;;AA6JH,uFAAuF;AACvF,uFAAuF;AACvF,oFAAoF;AACpF,oFAAoF;AACpF,qFAAqF;AACrF,QAAQ"}
@@ -0,0 +1,58 @@
1
+ import { type Cleanup } from "@xmachines/play";
2
+ import type { Readable } from "./types.js";
3
+ /**
4
+ * Subscribes to one atom, and coalesces the notifications into one microtask.
5
+ *
6
+ * `@xstate/store` notifies an observer SYNCHRONOUSLY from `set`. This function keeps
7
+ * that notification, and it defers the callback to a microtask. One batch of
8
+ * synchronous writes therefore calls the callback one time, with the last value. The
9
+ * cleanup function is idempotent.
10
+ *
11
+ * **The lifecycle:**
12
+ * - The `disposed` flag stops a callback after the cleanup: the microtask returns at
13
+ * once when the cleanup runs before the microtask fires.
14
+ * - The `scheduled` guard removes the duplicates of rapid synchronous writes: one
15
+ * batch of synchronous writes queues one microtask only.
16
+ * - The guard clears BEFORE the callback, and not after it. A callback that WRITES the
17
+ * atom that it watches therefore schedules the next delivery, and it reaches the
18
+ * caller with the value that it wrote. A callback that THROWS keeps the watch,
19
+ * because the throw leaves the microtask and never reaches the subscription.
20
+ *
21
+ * The library owns the dependency edges. `@xstate/store` builds the subscription on an
22
+ * effect, and the effect holds ONE edge to the atom for the life of the watch. A caller
23
+ * therefore arms the watch one time, and no call site of this function counts emissions
24
+ * or re-arms anything.
25
+ *
26
+ * A CAUTION for a callback that writes the atom that it watches: the write reaches the
27
+ * callback, so a callback that writes a NEW value on every call runs again for ever,
28
+ * and the microtask queue drains never. That is the loop of the caller, and this
29
+ * function bounds it not. A write of the SAME value ends the sequence by itself,
30
+ * because an atom notifies for a value that it holds already never.
31
+ *
32
+ * The return is a {@link Cleanup}: a function, and a `Disposable`. A caller that keeps
33
+ * the release in a field and runs it from a teardown keeps that code. A caller inside
34
+ * one scope writes `using` and writes no teardown:
35
+ *
36
+ * ```ts
37
+ * using stop = watchAtom(count, render);
38
+ * ```
39
+ *
40
+ * @param atom - The `Atom` or the `ReadonlyAtom` to subscribe to.
41
+ * @param onValue - The function reads the current atom value after each change.
42
+ * @returns The release. It removes the subscription.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * import { createAtom, watchAtom } from "@xmachines/play-atom";
47
+ *
48
+ * const count = createAtom(0);
49
+ * const stop = watchAtom(count, (value) => console.log(value));
50
+ *
51
+ * count.set(1);
52
+ * count.set(2); // One microtask, and the callback reads 2 one time.
53
+ *
54
+ * stop();
55
+ * ```
56
+ */
57
+ export declare function watchAtom<T>(atom: Readable<T>, onValue: (value: T) => void): Cleanup;
58
+ //# sourceMappingURL=watch-atom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch-atom.d.ts","sourceRoot":"","sources":["../src/watch-atom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CA0BpF"}
@@ -0,0 +1,84 @@
1
+ import { asCleanup } from "@xmachines/play";
2
+ /**
3
+ * Subscribes to one atom, and coalesces the notifications into one microtask.
4
+ *
5
+ * `@xstate/store` notifies an observer SYNCHRONOUSLY from `set`. This function keeps
6
+ * that notification, and it defers the callback to a microtask. One batch of
7
+ * synchronous writes therefore calls the callback one time, with the last value. The
8
+ * cleanup function is idempotent.
9
+ *
10
+ * **The lifecycle:**
11
+ * - The `disposed` flag stops a callback after the cleanup: the microtask returns at
12
+ * once when the cleanup runs before the microtask fires.
13
+ * - The `scheduled` guard removes the duplicates of rapid synchronous writes: one
14
+ * batch of synchronous writes queues one microtask only.
15
+ * - The guard clears BEFORE the callback, and not after it. A callback that WRITES the
16
+ * atom that it watches therefore schedules the next delivery, and it reaches the
17
+ * caller with the value that it wrote. A callback that THROWS keeps the watch,
18
+ * because the throw leaves the microtask and never reaches the subscription.
19
+ *
20
+ * The library owns the dependency edges. `@xstate/store` builds the subscription on an
21
+ * effect, and the effect holds ONE edge to the atom for the life of the watch. A caller
22
+ * therefore arms the watch one time, and no call site of this function counts emissions
23
+ * or re-arms anything.
24
+ *
25
+ * A CAUTION for a callback that writes the atom that it watches: the write reaches the
26
+ * callback, so a callback that writes a NEW value on every call runs again for ever,
27
+ * and the microtask queue drains never. That is the loop of the caller, and this
28
+ * function bounds it not. A write of the SAME value ends the sequence by itself,
29
+ * because an atom notifies for a value that it holds already never.
30
+ *
31
+ * The return is a {@link Cleanup}: a function, and a `Disposable`. A caller that keeps
32
+ * the release in a field and runs it from a teardown keeps that code. A caller inside
33
+ * one scope writes `using` and writes no teardown:
34
+ *
35
+ * ```ts
36
+ * using stop = watchAtom(count, render);
37
+ * ```
38
+ *
39
+ * @param atom - The `Atom` or the `ReadonlyAtom` to subscribe to.
40
+ * @param onValue - The function reads the current atom value after each change.
41
+ * @returns The release. It removes the subscription.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { createAtom, watchAtom } from "@xmachines/play-atom";
46
+ *
47
+ * const count = createAtom(0);
48
+ * const stop = watchAtom(count, (value) => console.log(value));
49
+ *
50
+ * count.set(1);
51
+ * count.set(2); // One microtask, and the callback reads 2 one time.
52
+ *
53
+ * stop();
54
+ * ```
55
+ */
56
+ export function watchAtom(atom, onValue) {
57
+ let disposed = false;
58
+ let scheduled = false;
59
+ const subscription = atom.subscribe(() => {
60
+ if (disposed || scheduled)
61
+ return;
62
+ scheduled = true;
63
+ queueMicrotask(() => {
64
+ if (disposed)
65
+ return;
66
+ // Clear the guard BEFORE the callback, and not after it. A callback that WRITES
67
+ // the atom notifies the subscription on that write — a renderer that gives its
68
+ // host an error handler, and the host repairs the actor from that handler. The
69
+ // guard must accept that notification at this moment. A line after the callback
70
+ // clears it too late: the write finds the guard still set, the observer drops
71
+ // the notification, and the line then clears the guard for a change that reaches
72
+ // nobody.
73
+ scheduled = false;
74
+ onValue(atom.get());
75
+ });
76
+ });
77
+ return asCleanup(() => {
78
+ if (disposed)
79
+ return;
80
+ disposed = true;
81
+ subscription.unsubscribe();
82
+ });
83
+ }
84
+ //# sourceMappingURL=watch-atom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch-atom.js","sourceRoot":"","sources":["../src/watch-atom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,iBAAiB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,UAAU,SAAS,CAAI,IAAiB,EAAE,OAA2B;IAC1E,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;QACxC,IAAI,QAAQ,IAAI,SAAS;YAAE,OAAO;QAClC,SAAS,GAAG,IAAI,CAAC;QACjB,cAAc,CAAC,GAAG,EAAE;YACnB,IAAI,QAAQ;gBAAE,OAAO;YACrB,gFAAgF;YAChF,+EAA+E;YAC/E,+EAA+E;YAC/E,gFAAgF;YAChF,8EAA8E;YAC9E,iFAAiF;YACjF,UAAU;YACV,SAAS,GAAG,KAAK,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,GAAG,EAAE;QACrB,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,YAAY,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@xmachines/play-atom",
3
+ "version": "5.0.0",
4
+ "private": false,
5
+ "description": "Atom primitives for XMachines - fine-grained reactive state on @xstate/store",
6
+ "keywords": [
7
+ "atom",
8
+ "reactive",
9
+ "state-management",
10
+ "xmachines",
11
+ "xstate"
12
+ ],
13
+ "homepage": "https://gitlab.com/xmachin-es/xmachines-js/tree/main/packages/play-atom",
14
+ "license": "MIT",
15
+ "author": "XMachines Contributors",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://gitlab.com/xmachin-es/xmachines-js.git",
19
+ "directory": "packages/play-atom"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "vite build && tsc --build",
42
+ "clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__ node_modules/.svelte2tsx-* node_modules/.vite*",
43
+ "test": "vitest",
44
+ "test:coverage": "vitest run --coverage",
45
+ "lint": "oxlint .",
46
+ "lint:security": "node ../../scripts/semgrep-scan.mjs",
47
+ "lint:fix": "oxlint --fix .",
48
+ "format": "oxfmt .",
49
+ "format:check": "oxfmt --check ."
50
+ },
51
+ "devDependencies": {
52
+ "@testing-library/jest-dom": "^7.0.1",
53
+ "@types/node": "^26.6.2",
54
+ "@vitest/browser-playwright": "^5.0.1",
55
+ "@xmachines/play": "5.0.0",
56
+ "@xstate/store": "^4.2.3",
57
+ "oxfmt": "^0.70.0",
58
+ "oxlint": "^1.85.0",
59
+ "vite": "^8.3.0",
60
+ "vitest": "^5.0.1"
61
+ },
62
+ "peerDependencies": {
63
+ "@xmachines/play": "5.0.0",
64
+ "@xstate/store": "^4.2.3"
65
+ },
66
+ "engines": {
67
+ "node": ">=24.0.0"
68
+ }
69
+ }