@uniflowed/state 0.0.0-alpha.18
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/index.js +691 -0
- package/internal/atom.js +213 -0
- package/internal/composed.js +882 -0
- package/internal/provider.js +64 -0
- package/internal/store.js +359 -0
- package/package.json +25 -0
package/internal/atom.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// What an atom is before any store exists.
|
|
4
|
+
//
|
|
5
|
+
// An atom is a *definition*, not a value. `atom(0)` allocates no state,
|
|
6
|
+
// belongs to nothing, and can be declared at module scope in a file that a
|
|
7
|
+
// server imports. The value lives in a store, and the same definition has a
|
|
8
|
+
// different value in every store that ever instantiates it.
|
|
9
|
+
//
|
|
10
|
+
// # Why definitions and values are separated
|
|
11
|
+
//
|
|
12
|
+
// The alternative — an atom that holds its own value, which is what this
|
|
13
|
+
// package used to ship — is simpler right up to the point where two of
|
|
14
|
+
// something must exist at once, and then it is unfixable:
|
|
15
|
+
//
|
|
16
|
+
// * a server renders two requests concurrently in one process, and module
|
|
17
|
+
// state is shared between them, so one user's cart is the other user's cart;
|
|
18
|
+
// * a test writes an atom and the next test in the same file inherits it;
|
|
19
|
+
// * a component tree wants its own copy — a preview pane, a modal editing a
|
|
20
|
+
// draft of what the page behind it shows — and there is no copy to have.
|
|
21
|
+
//
|
|
22
|
+
// A store is the unit of isolation those three want, and `<Provider>` is how a
|
|
23
|
+
// React subtree picks one. This is also Jotai's model, for the same reasons.
|
|
24
|
+
//
|
|
25
|
+
// # Why the record carries functions rather than a class
|
|
26
|
+
//
|
|
27
|
+
// A definition is a plain frozen-shaped record of the two functions a store
|
|
28
|
+
// needs — how to read it, and what happens when it is written — so the store
|
|
29
|
+
// is a `WeakMap` lookup plus a call, and an atom that is never read costs one
|
|
30
|
+
// object. There is no base class to extend and no registry holding a reference
|
|
31
|
+
// to every atom ever declared, which is what lets an `atomFamily` member be
|
|
32
|
+
// collected once the key is gone.
|
|
33
|
+
|
|
34
|
+
import type { LoadContext, Unsubscribe } from "@uniflowed/cell";
|
|
35
|
+
|
|
36
|
+
/** What a setter accepts: a value, or a reducer over the current one. */
|
|
37
|
+
export type SetAction<T> = T | ((current: T) => T);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The three states an asynchronous read can be in.
|
|
41
|
+
*
|
|
42
|
+
* A discriminated union rather than `{ loading, data, error }` with three
|
|
43
|
+
* optional fields, because two of those eight combinations are nonsense and
|
|
44
|
+
* `match` can prove this one is exhaustive.
|
|
45
|
+
*/
|
|
46
|
+
export type Loadable<T> =
|
|
47
|
+
| { readonly state: "loading" }
|
|
48
|
+
| { readonly state: "hasData", readonly data: T }
|
|
49
|
+
| { readonly state: "hasError", readonly error: mixed };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What an atom's `onMount` is handed: the atom, bound to the store it was
|
|
53
|
+
* mounted in.
|
|
54
|
+
*
|
|
55
|
+
* `set` goes through the store's ordinary write path, so an atom that mounts a
|
|
56
|
+
* subscription feeds values in exactly the way an event handler would.
|
|
57
|
+
*/
|
|
58
|
+
export type AtomMount<T> = {
|
|
59
|
+
readonly get: () => T,
|
|
60
|
+
readonly set: (value: T) => void,
|
|
61
|
+
readonly subscribe: (listener: () => void) => Unsubscribe,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reading another atom, from inside a read or a write.
|
|
66
|
+
*
|
|
67
|
+
* Inside a read this is also what records the dependency — which is why a read
|
|
68
|
+
* that branches depends on the branch it took and not on the other one.
|
|
69
|
+
*/
|
|
70
|
+
export type AtomGetter = <V>(target: AtomRecord<V, empty>) => V;
|
|
71
|
+
|
|
72
|
+
/** Writing another atom, from inside a write. */
|
|
73
|
+
export type AtomSetter = <V, A>(target: AtomRecord<V, A>, arg: A) => void;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An atom, as the store sees it.
|
|
77
|
+
*
|
|
78
|
+
* Every field is read-only, which is what makes `AtomRecord<T, A>` a subtype
|
|
79
|
+
* of `AtomRecord<T, empty>`: a writable atom is accepted anywhere a readable
|
|
80
|
+
* one is, and the argument type stays exact at the call site rather than
|
|
81
|
+
* widening to `mixed` the moment an atom is passed somewhere general.
|
|
82
|
+
*/
|
|
83
|
+
export type AtomRecord<T, A> = {
|
|
84
|
+
readonly kind: "primitive" | "derived" | "async",
|
|
85
|
+
/** For diagnostics only. Never load-bearing. */
|
|
86
|
+
readonly label: string,
|
|
87
|
+
/** The value before anything is computed: a primitive's initial value, an
|
|
88
|
+
* async atom's `loading`, a write-only atom's `null`. */
|
|
89
|
+
readonly initial: T,
|
|
90
|
+
readonly read: null | ((get: AtomGetter) => T),
|
|
91
|
+
readonly load: null | ((get: AtomGetter, context: LoadContext) => Promise<T>),
|
|
92
|
+
readonly write: null | ((get: AtomGetter, set: AtomSetter, arg: A) => void),
|
|
93
|
+
readonly equals: null | ((previous: T, next: T) => boolean),
|
|
94
|
+
readonly onMount: null | ((mount: AtomMount<T>) => void | (() => void)),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** What every constructor accepts. */
|
|
98
|
+
export type AtomOptions<T> = {
|
|
99
|
+
readonly debugLabel?: string,
|
|
100
|
+
readonly equals?: (previous: T, next: T) => boolean,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** What a primitive atom accepts, which is a mount as well. */
|
|
104
|
+
export type PrimitiveOptions<T> = {
|
|
105
|
+
readonly debugLabel?: string,
|
|
106
|
+
readonly equals?: (previous: T, next: T) => boolean,
|
|
107
|
+
readonly onMount?: (mount: AtomMount<T>) => void | (() => void),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The fields every kind of atom shares, filled in from whichever options that
|
|
112
|
+
* kind accepts.
|
|
113
|
+
*
|
|
114
|
+
* The options parameter is inexact on purpose: a primitive's options carry an
|
|
115
|
+
* `onMount` that the other kinds do not have, and this is the one place all of
|
|
116
|
+
* them meet.
|
|
117
|
+
*/
|
|
118
|
+
function baseRecord<T, A>(
|
|
119
|
+
kind: "primitive" | "derived" | "async",
|
|
120
|
+
label: string,
|
|
121
|
+
initial: T,
|
|
122
|
+
options: void | {
|
|
123
|
+
readonly debugLabel?: string,
|
|
124
|
+
readonly equals?: (previous: T, next: T) => boolean,
|
|
125
|
+
...
|
|
126
|
+
},
|
|
127
|
+
): AtomRecord<T, A> {
|
|
128
|
+
return {
|
|
129
|
+
kind,
|
|
130
|
+
label: options?.debugLabel ?? label,
|
|
131
|
+
initial,
|
|
132
|
+
read: null,
|
|
133
|
+
load: null,
|
|
134
|
+
write: null,
|
|
135
|
+
equals: options?.equals ?? null,
|
|
136
|
+
onMount: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A value a store holds directly. */
|
|
141
|
+
export function definePrimitive<T>(
|
|
142
|
+
initial: T,
|
|
143
|
+
options?: PrimitiveOptions<T>,
|
|
144
|
+
): AtomRecord<T, SetAction<T>> {
|
|
145
|
+
return {
|
|
146
|
+
...baseRecord("primitive", "atom", initial, options),
|
|
147
|
+
onMount: options?.onMount ?? null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A value computed from other atoms, with an optional write of its own.
|
|
153
|
+
*
|
|
154
|
+
* The record's `initial` is never read for one of these — a selector's value
|
|
155
|
+
* comes from running `read` — but the field exists for the kinds that do have
|
|
156
|
+
* one, so the placeholder is cast here, once, rather than at every call site.
|
|
157
|
+
*/
|
|
158
|
+
export function defineSelector<T, A>(
|
|
159
|
+
read: (get: AtomGetter) => T,
|
|
160
|
+
write: null | ((get: AtomGetter, set: AtomSetter, argument: A) => void),
|
|
161
|
+
options?: AtomOptions<T>,
|
|
162
|
+
): AtomRecord<T, A> {
|
|
163
|
+
const unevaluated: T = null as $FlowFixMe;
|
|
164
|
+
return { ...baseRecord("derived", "selector", unevaluated, options), read, write };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A write with no value: an action.
|
|
169
|
+
*
|
|
170
|
+
* Its value really is `null` rather than a placeholder, so reading one is
|
|
171
|
+
* defined behaviour, and a component that only dispatches never subscribes to
|
|
172
|
+
* anything.
|
|
173
|
+
*/
|
|
174
|
+
export function defineAction<A>(
|
|
175
|
+
write: (get: AtomGetter, set: AtomSetter, argument: A) => void,
|
|
176
|
+
options?: AtomOptions<null>,
|
|
177
|
+
): AtomRecord<null, A> {
|
|
178
|
+
return { ...baseRecord("derived", "action", null, options), write };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A value that arrives from a promise, projected into a [`Loadable`].
|
|
183
|
+
*
|
|
184
|
+
* The rejection is folded into the value here, at definition time, rather than
|
|
185
|
+
* left for the store: a promise that rejects with nobody attached is an
|
|
186
|
+
* unhandled rejection, and the store attaches its handler one turn later than
|
|
187
|
+
* this does.
|
|
188
|
+
*
|
|
189
|
+
* Which is also why the abandoned-load check cannot live here. Folding turns
|
|
190
|
+
* every rejection into a *resolution* carrying `{ state: "hasError" }`, so by
|
|
191
|
+
* the time a value reaches the cell there is nothing left to recognise an
|
|
192
|
+
* aborted load by. The cell decides whether a settlement still speaks for it,
|
|
193
|
+
* before this projection is ever consulted — see `@uniflowed/cell`'s
|
|
194
|
+
* `internal/resource.js`.
|
|
195
|
+
*/
|
|
196
|
+
export function defineAsync<T>(
|
|
197
|
+
load: (get: AtomGetter, context: LoadContext) => Promise<T>,
|
|
198
|
+
options?: AtomOptions<Loadable<T>>,
|
|
199
|
+
): AtomRecord<Loadable<T>, empty> {
|
|
200
|
+
const pending: Loadable<T> = { state: "loading" };
|
|
201
|
+
return {
|
|
202
|
+
...baseRecord("async", "asyncAtom", pending, options),
|
|
203
|
+
load: (get, context) => load(get, context).then(asData, asError),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function asData<T>(data: T): Loadable<T> {
|
|
208
|
+
return { state: "hasData", data };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function asError<T>(error: mixed): Loadable<T> {
|
|
212
|
+
return { state: "hasError", error };
|
|
213
|
+
}
|