@fluixi/reactive 1.0.0-alpha.100
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 +21 -0
- package/README.md +280 -0
- package/dist/cdn/reactive.cjs +2803 -0
- package/dist/cdn/reactive.d.ts +11 -0
- package/dist/cdn/reactive.d.ts.map +1 -0
- package/dist/cdn/reactive.global.js +2 -0
- package/dist/cdn/reactive.js +10 -0
- package/dist/cdn/reactive.mjs +2787 -0
- package/dist/cdn/signal.cjs +299 -0
- package/dist/cdn/signal.d.ts +19 -0
- package/dist/cdn/signal.d.ts.map +1 -0
- package/dist/cdn/signal.global.js +2 -0
- package/dist/cdn/signal.js +18 -0
- package/dist/cdn/signal.mjs +276 -0
- package/dist/index.cjs +2803 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +2787 -0
- package/dist/lib/index.cjs +2804 -0
- package/dist/lib/index.d.ts +8 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +11 -0
- package/dist/lib/index.mjs +2789 -0
- package/dist/lib/observable/index.cjs +386 -0
- package/dist/lib/observable/index.d.ts +4 -0
- package/dist/lib/observable/index.d.ts.map +1 -0
- package/dist/lib/observable/index.js +3 -0
- package/dist/lib/observable/index.mjs +364 -0
- package/dist/lib/observable/observable.cjs +384 -0
- package/dist/lib/observable/observable.d.ts +246 -0
- package/dist/lib/observable/observable.d.ts.map +1 -0
- package/dist/lib/observable/observable.js +268 -0
- package/dist/lib/observable/observable.mjs +361 -0
- package/dist/lib/observable/subscription.cjs +180 -0
- package/dist/lib/observable/subscription.d.ts +58 -0
- package/dist/lib/observable/subscription.d.ts.map +1 -0
- package/dist/lib/observable/subscription.js +177 -0
- package/dist/lib/observable/subscription.mjs +157 -0
- package/dist/lib/observable/types.cjs +18 -0
- package/dist/lib/observable/types.d.ts +44 -0
- package/dist/lib/observable/types.d.ts.map +1 -0
- package/dist/lib/observable/types.js +1 -0
- package/dist/lib/observable/types.mjs +0 -0
- package/dist/lib/predicates.cjs +44 -0
- package/dist/lib/predicates.d.ts +37 -0
- package/dist/lib/predicates.d.ts.map +1 -0
- package/dist/lib/predicates.js +62 -0
- package/dist/lib/predicates.mjs +23 -0
- package/dist/lib/signal/api.cjs +592 -0
- package/dist/lib/signal/api.d.ts +227 -0
- package/dist/lib/signal/api.d.ts.map +1 -0
- package/dist/lib/signal/api.js +228 -0
- package/dist/lib/signal/api.mjs +569 -0
- package/dist/lib/signal/cached-resource.cjs +726 -0
- package/dist/lib/signal/cached-resource.d.ts +86 -0
- package/dist/lib/signal/cached-resource.d.ts.map +1 -0
- package/dist/lib/signal/cached-resource.js +250 -0
- package/dist/lib/signal/cached-resource.mjs +704 -0
- package/dist/lib/signal/graph/children.cjs +326 -0
- package/dist/lib/signal/graph/children.d.ts +2 -0
- package/dist/lib/signal/graph/children.d.ts.map +1 -0
- package/dist/lib/signal/graph/children.js +30 -0
- package/dist/lib/signal/graph/children.mjs +303 -0
- package/dist/lib/signal/graph/compat.cjs +590 -0
- package/dist/lib/signal/graph/compat.d.ts +66 -0
- package/dist/lib/signal/graph/compat.d.ts.map +1 -0
- package/dist/lib/signal/graph/compat.js +197 -0
- package/dist/lib/signal/graph/compat.mjs +568 -0
- package/dist/lib/signal/graph/core.cjs +295 -0
- package/dist/lib/signal/graph/core.d.ts +99 -0
- package/dist/lib/signal/graph/core.d.ts.map +1 -0
- package/dist/lib/signal/graph/core.js +268 -0
- package/dist/lib/signal/graph/core.mjs +273 -0
- package/dist/lib/signal/graph/debounced.cjs +680 -0
- package/dist/lib/signal/graph/debounced.d.ts +67 -0
- package/dist/lib/signal/graph/debounced.d.ts.map +1 -0
- package/dist/lib/signal/graph/debounced.js +92 -0
- package/dist/lib/signal/graph/debounced.mjs +657 -0
- package/dist/lib/signal/graph/derived.cjs +508 -0
- package/dist/lib/signal/graph/derived.d.ts +14 -0
- package/dist/lib/signal/graph/derived.d.ts.map +1 -0
- package/dist/lib/signal/graph/derived.js +65 -0
- package/dist/lib/signal/graph/derived.mjs +485 -0
- package/dist/lib/signal/graph/detect.cjs +66 -0
- package/dist/lib/signal/graph/detect.d.ts +17 -0
- package/dist/lib/signal/graph/detect.d.ts.map +1 -0
- package/dist/lib/signal/graph/detect.js +39 -0
- package/dist/lib/signal/graph/detect.mjs +45 -0
- package/dist/lib/signal/graph/error-boundary.cjs +313 -0
- package/dist/lib/signal/graph/error-boundary.d.ts +16 -0
- package/dist/lib/signal/graph/error-boundary.d.ts.map +1 -0
- package/dist/lib/signal/graph/error-boundary.js +34 -0
- package/dist/lib/signal/graph/error-boundary.mjs +290 -0
- package/dist/lib/signal/graph/index.cjs +305 -0
- package/dist/lib/signal/graph/index.d.ts +22 -0
- package/dist/lib/signal/graph/index.d.ts.map +1 -0
- package/dist/lib/signal/graph/index.js +21 -0
- package/dist/lib/signal/graph/index.mjs +283 -0
- package/dist/lib/signal/graph/observe.cjs +198 -0
- package/dist/lib/signal/graph/observe.d.ts +185 -0
- package/dist/lib/signal/graph/observe.d.ts.map +1 -0
- package/dist/lib/signal/graph/observe.js +198 -0
- package/dist/lib/signal/graph/observe.mjs +175 -0
- package/dist/lib/signal/graph/resource.cjs +632 -0
- package/dist/lib/signal/graph/resource.d.ts +39 -0
- package/dist/lib/signal/graph/resource.d.ts.map +1 -0
- package/dist/lib/signal/graph/resource.js +165 -0
- package/dist/lib/signal/graph/resource.mjs +609 -0
- package/dist/lib/signal/graph/runtime.cjs +522 -0
- package/dist/lib/signal/graph/runtime.d.ts +80 -0
- package/dist/lib/signal/graph/runtime.d.ts.map +1 -0
- package/dist/lib/signal/graph/runtime.js +358 -0
- package/dist/lib/signal/graph/runtime.mjs +499 -0
- package/dist/lib/signal/graph/signal-next.cjs +1249 -0
- package/dist/lib/signal/graph/signal-next.d.ts +38 -0
- package/dist/lib/signal/graph/signal-next.d.ts.map +1 -0
- package/dist/lib/signal/graph/signal-next.js +44 -0
- package/dist/lib/signal/graph/signal-next.mjs +1227 -0
- package/dist/lib/signal/graph/ssr-hooks.cjs +89 -0
- package/dist/lib/signal/graph/ssr-hooks.d.ts +51 -0
- package/dist/lib/signal/graph/ssr-hooks.d.ts.map +1 -0
- package/dist/lib/signal/graph/ssr-hooks.js +80 -0
- package/dist/lib/signal/graph/ssr-hooks.mjs +68 -0
- package/dist/lib/signal/graph/state.cjs +72 -0
- package/dist/lib/signal/graph/state.d.ts +76 -0
- package/dist/lib/signal/graph/state.d.ts.map +1 -0
- package/dist/lib/signal/graph/state.js +99 -0
- package/dist/lib/signal/graph/state.mjs +49 -0
- package/dist/lib/signal/graph/suspense.cjs +313 -0
- package/dist/lib/signal/graph/suspense.d.ts +13 -0
- package/dist/lib/signal/graph/suspense.d.ts.map +1 -0
- package/dist/lib/signal/graph/suspense.js +36 -0
- package/dist/lib/signal/graph/suspense.mjs +290 -0
- package/dist/lib/signal/index-legacy.cjs +2582 -0
- package/dist/lib/signal/index-legacy.d.ts +12 -0
- package/dist/lib/signal/index-legacy.d.ts.map +1 -0
- package/dist/lib/signal/index-legacy.js +11 -0
- package/dist/lib/signal/index-legacy.mjs +2560 -0
- package/dist/lib/signal/index.cjs +1350 -0
- package/dist/lib/signal/index.d.ts +19 -0
- package/dist/lib/signal/index.d.ts.map +1 -0
- package/dist/lib/signal/index.js +19 -0
- package/dist/lib/signal/index.mjs +1328 -0
- package/dist/lib/signal/list.cjs +561 -0
- package/dist/lib/signal/list.d.ts +39 -0
- package/dist/lib/signal/list.d.ts.map +1 -0
- package/dist/lib/signal/list.js +172 -0
- package/dist/lib/signal/list.mjs +538 -0
- package/dist/lib/signal/resource-api.cjs +638 -0
- package/dist/lib/signal/resource-api.d.ts +19 -0
- package/dist/lib/signal/resource-api.d.ts.map +1 -0
- package/dist/lib/signal/resource-api.js +27 -0
- package/dist/lib/signal/resource-api.mjs +615 -0
- package/dist/lib/signal/signal.cjs +2035 -0
- package/dist/lib/signal/signal.d.ts +303 -0
- package/dist/lib/signal/signal.d.ts.map +1 -0
- package/dist/lib/signal/signal.js +1233 -0
- package/dist/lib/signal/signal.mjs +2012 -0
- package/dist/lib/signal/types.cjs +38 -0
- package/dist/lib/signal/types.d.ts +133 -0
- package/dist/lib/signal/types.d.ts.map +1 -0
- package/dist/lib/signal/types.js +38 -0
- package/dist/lib/signal/types.mjs +17 -0
- package/dist/lib/signal/utilities.cjs +1024 -0
- package/dist/lib/signal/utilities.d.ts +247 -0
- package/dist/lib/signal/utilities.d.ts.map +1 -0
- package/dist/lib/signal/utilities.js +346 -0
- package/dist/lib/signal/utilities.mjs +1002 -0
- package/dist/lib/store/api.cjs +662 -0
- package/dist/lib/store/api.d.ts +40 -0
- package/dist/lib/store/api.d.ts.map +1 -0
- package/dist/lib/store/api.js +63 -0
- package/dist/lib/store/api.mjs +646 -0
- package/dist/lib/store/index.cjs +741 -0
- package/dist/lib/store/index.d.ts +16 -0
- package/dist/lib/store/index.d.ts.map +1 -0
- package/dist/lib/store/index.js +13 -0
- package/dist/lib/store/index.mjs +726 -0
- package/dist/lib/store/setter.types.cjs +18 -0
- package/dist/lib/store/setter.types.d.ts +131 -0
- package/dist/lib/store/setter.types.d.ts.map +1 -0
- package/dist/lib/store/setter.types.js +65 -0
- package/dist/lib/store/setter.types.mjs +0 -0
- package/dist/lib/store/store.cjs +708 -0
- package/dist/lib/store/store.d.ts +142 -0
- package/dist/lib/store/store.d.ts.map +1 -0
- package/dist/lib/store/store.js +812 -0
- package/dist/lib/store/store.mjs +694 -0
- package/dist/lib/store/types.cjs +18 -0
- package/dist/lib/store/types.d.ts +57 -0
- package/dist/lib/store/types.d.ts.map +1 -0
- package/dist/lib/store/types.js +1 -0
- package/dist/lib/store/types.mjs +0 -0
- package/dist/lib/store/utils.cjs +43 -0
- package/dist/lib/store/utils.d.ts +13 -0
- package/dist/lib/store/utils.d.ts.map +1 -0
- package/dist/lib/store/utils.js +29 -0
- package/dist/lib/store/utils.mjs +22 -0
- package/dist/lib/subject.cjs +450 -0
- package/dist/lib/subject.d.ts +14 -0
- package/dist/lib/subject.d.ts.map +1 -0
- package/dist/lib/subject.js +81 -0
- package/dist/lib/subject.mjs +427 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/version.generated.cjs +27 -0
- package/dist/version.generated.d.ts +3 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +3 -0
- package/dist/version.generated.mjs +7 -0
- package/package.json +115 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { $PROXY, batch, createEffect, createMemo, createSignal, untrack, } from '@fluixi/reactive/signal';
|
|
2
|
+
import { isObserved, reportReactiveDispose, reportReactiveMove, reportReactiveNode, withReactiveOrigin, } from '../signal/graph/observe.js';
|
|
3
|
+
export const $RAW = Symbol('store-raw'), $NODE = Symbol('store-node'), $HAS = Symbol('store-has'), $SELF = Symbol('store-self'), $TRACK = Symbol('store-track'), $NAME = Symbol('store-name'), $STORE = Symbol('store-store');
|
|
4
|
+
const NODES = new WeakMap();
|
|
5
|
+
const PROXIES = new WeakMap();
|
|
6
|
+
const OWNERS = new WeakMap();
|
|
7
|
+
/** Every tracked object back to the state it belongs to, for deciding what is still in use. */
|
|
8
|
+
const ROOTS = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* Say goodbye to the signals of an object a write replaced.
|
|
11
|
+
*
|
|
12
|
+
* `set('items', [...items, one])` drops the old array but keeps the objects in it, so what
|
|
13
|
+
* is still reachable from the state keeps its nodes and only the rest is let go. Without
|
|
14
|
+
* this a watcher accumulates a card per key of every array a list ever had.
|
|
15
|
+
*
|
|
16
|
+
* Only when something is watching: this walks the state, and nothing else needs it.
|
|
17
|
+
*/
|
|
18
|
+
function forgetReplaced(dead, state) {
|
|
19
|
+
if (!isTrackable(dead) || !isObserved())
|
|
20
|
+
return;
|
|
21
|
+
const alive = new Set();
|
|
22
|
+
const keep = (value) => {
|
|
23
|
+
if (!isTrackable(value) || alive.has(value))
|
|
24
|
+
return;
|
|
25
|
+
alive.add(value);
|
|
26
|
+
for (const child of Object.values(value))
|
|
27
|
+
keep(child);
|
|
28
|
+
};
|
|
29
|
+
keep(ROOTS.get(state) ?? state);
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const drop = (value) => {
|
|
32
|
+
if (!isTrackable(value))
|
|
33
|
+
return;
|
|
34
|
+
const target = value;
|
|
35
|
+
if (alive.has(target) || seen.has(target))
|
|
36
|
+
return;
|
|
37
|
+
seen.add(target);
|
|
38
|
+
const ns = NODES.get(target);
|
|
39
|
+
if (ns) {
|
|
40
|
+
for (const node of ns.keys.values())
|
|
41
|
+
reportReactiveDispose(node);
|
|
42
|
+
if (ns.all)
|
|
43
|
+
reportReactiveDispose(ns.all);
|
|
44
|
+
NODES.delete(target);
|
|
45
|
+
OWNERS.delete(target);
|
|
46
|
+
PROXIES.delete(target);
|
|
47
|
+
}
|
|
48
|
+
for (const child of Object.values(target))
|
|
49
|
+
drop(child);
|
|
50
|
+
};
|
|
51
|
+
drop(dead);
|
|
52
|
+
}
|
|
53
|
+
const pathOf = (base, key, array) => array ? `${base}[${String(key)}]` : `${base}.${String(key)}`;
|
|
54
|
+
function createDataNode(initial, ns, key) {
|
|
55
|
+
// A key signal is made the first time something reads it, which can be anywhere. Named and
|
|
56
|
+
// attributed to the object it belongs to, so a graph shows it under that object at the
|
|
57
|
+
// line the store was declared rather than wherever it was first read.
|
|
58
|
+
const owner = ns?.owner;
|
|
59
|
+
const label = owner && key !== undefined ? pathOf(owner.name, key, ns?.array) : undefined;
|
|
60
|
+
const [get, set] = withReactiveOrigin(owner?.store, () => createSignal(initial, label ? { name: label } : undefined));
|
|
61
|
+
const node = get;
|
|
62
|
+
node.$ = set;
|
|
63
|
+
return node;
|
|
64
|
+
}
|
|
65
|
+
function nodesOf(target) {
|
|
66
|
+
let ns = NODES.get(target);
|
|
67
|
+
if (!ns)
|
|
68
|
+
NODES.set(target, (ns = { keys: new Map(), owner: OWNERS.get(target), array: Array.isArray(target) }));
|
|
69
|
+
// An object can be reached before the store has claimed it, a write touches it, or it is
|
|
70
|
+
// read through something other than its parent's proxy. Caching "no owner" there would
|
|
71
|
+
// leave every key signal it ever makes unnamed and unattached.
|
|
72
|
+
else if (!ns.owner)
|
|
73
|
+
ns.owner = OWNERS.get(target);
|
|
74
|
+
return ns;
|
|
75
|
+
}
|
|
76
|
+
function keyNode(ns, key, seed) {
|
|
77
|
+
let node = ns.keys.get(key);
|
|
78
|
+
if (!node)
|
|
79
|
+
ns.keys.set(key, (node = createDataNode(seed, ns, key)));
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Say where an object now sits in the state.
|
|
84
|
+
*
|
|
85
|
+
* A write that replaces a container leaves the objects inside it where they were, holding
|
|
86
|
+
* the same signals, under a new key signal. Re-claiming moves those signals across so they
|
|
87
|
+
* stay attached to the object they describe instead of to the node that used to hold it.
|
|
88
|
+
*/
|
|
89
|
+
function claim(target, holder, name, parent) {
|
|
90
|
+
const was = OWNERS.get(target);
|
|
91
|
+
if (was && was.store === holder && was.name === name)
|
|
92
|
+
return;
|
|
93
|
+
const owner = { store: holder, name };
|
|
94
|
+
OWNERS.set(target, owner);
|
|
95
|
+
ROOTS.set(target, ROOTS.get(parent) ?? parent);
|
|
96
|
+
// It may already have a node store, from an earlier read or an earlier position.
|
|
97
|
+
const ns = NODES.get(target);
|
|
98
|
+
if (!ns)
|
|
99
|
+
return;
|
|
100
|
+
ns.owner = owner;
|
|
101
|
+
if (!isObserved())
|
|
102
|
+
return;
|
|
103
|
+
for (const [key, node] of ns.keys)
|
|
104
|
+
reportReactiveMove(node, holder, pathOf(name, key, ns.array));
|
|
105
|
+
if (ns.all)
|
|
106
|
+
reportReactiveMove(ns.all, holder, pathOf(name, '*', ns.array));
|
|
107
|
+
}
|
|
108
|
+
function trackKeys(target) {
|
|
109
|
+
const ns = nodesOf(target);
|
|
110
|
+
// Which keys exist, as opposed to what any one of them holds. It has no value of its own,
|
|
111
|
+
// so a graph would otherwise show an unexplained empty node the first time a list is
|
|
112
|
+
// iterated, which is exactly when one appears.
|
|
113
|
+
(ns.all || (ns.all = createDataNode(undefined, ns, '*')))();
|
|
114
|
+
}
|
|
115
|
+
// The signal setter reads a function argument as an updater, so a function value
|
|
116
|
+
// has to be handed over wrapped.
|
|
117
|
+
function pokeNode(node, value) {
|
|
118
|
+
node.$(typeof value === 'function' ? () => value : value);
|
|
119
|
+
}
|
|
120
|
+
function isTrackable(value) {
|
|
121
|
+
if (value == null || typeof value !== 'object')
|
|
122
|
+
return false;
|
|
123
|
+
if (Array.isArray(value))
|
|
124
|
+
return true;
|
|
125
|
+
const proto = Object.getPrototypeOf(value);
|
|
126
|
+
return proto === Object.prototype || proto === null;
|
|
127
|
+
}
|
|
128
|
+
function box(value) {
|
|
129
|
+
if (!isTrackable(value))
|
|
130
|
+
return value;
|
|
131
|
+
const existing = PROXIES.get(value);
|
|
132
|
+
if (existing)
|
|
133
|
+
return existing;
|
|
134
|
+
const proxy = new Proxy(value, handler);
|
|
135
|
+
PROXIES.set(value, proxy);
|
|
136
|
+
return proxy;
|
|
137
|
+
}
|
|
138
|
+
const handler = {
|
|
139
|
+
get(target, key, receiver) {
|
|
140
|
+
if (key === $RAW)
|
|
141
|
+
return target;
|
|
142
|
+
if (key === $NODE)
|
|
143
|
+
return target;
|
|
144
|
+
if (key === $PROXY)
|
|
145
|
+
return receiver;
|
|
146
|
+
if (key === $HAS)
|
|
147
|
+
return (k) => k in target;
|
|
148
|
+
if (key === '__proxy')
|
|
149
|
+
return true;
|
|
150
|
+
if (key === $TRACK || key === $SELF) {
|
|
151
|
+
trackKeys(target);
|
|
152
|
+
return receiver;
|
|
153
|
+
}
|
|
154
|
+
const value = target[key];
|
|
155
|
+
// Prototype methods (array iterators/mutators and the like) go back untouched
|
|
156
|
+
// so they run with the proxy as `this` and their element reads track per key.
|
|
157
|
+
if (typeof value === 'function' &&
|
|
158
|
+
!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
const desc = Object.getOwnPropertyDescriptor(target, key);
|
|
162
|
+
if (desc && desc.get)
|
|
163
|
+
return desc.get.call(receiver);
|
|
164
|
+
const ns = nodesOf(target);
|
|
165
|
+
const holder = keyNode(ns, key, value);
|
|
166
|
+
const current = holder();
|
|
167
|
+
if (!isTrackable(current))
|
|
168
|
+
return current;
|
|
169
|
+
// A nested object hangs off the signal that holds it, not off the store, so a cart with
|
|
170
|
+
// three items reads as three objects rather than as a dozen loose keys.
|
|
171
|
+
if (ns.owner)
|
|
172
|
+
claim(current, holder, pathOf(ns.owner.name, key, ns.array), target);
|
|
173
|
+
return box(current);
|
|
174
|
+
},
|
|
175
|
+
set() {
|
|
176
|
+
// The proxy is read-only; writes come in through the setter.
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
deleteProperty(target, key) {
|
|
180
|
+
if (key in target)
|
|
181
|
+
writeKey(target, key, undefined);
|
|
182
|
+
return true;
|
|
183
|
+
},
|
|
184
|
+
has(target, key) {
|
|
185
|
+
if (key === $RAW ||
|
|
186
|
+
key === $PROXY ||
|
|
187
|
+
key === $NODE ||
|
|
188
|
+
key === $HAS ||
|
|
189
|
+
key === $SELF ||
|
|
190
|
+
key === $TRACK)
|
|
191
|
+
return true;
|
|
192
|
+
keyNode(nodesOf(target), key, target[key])();
|
|
193
|
+
return key in target;
|
|
194
|
+
},
|
|
195
|
+
ownKeys(target) {
|
|
196
|
+
trackKeys(target);
|
|
197
|
+
return Reflect.ownKeys(target);
|
|
198
|
+
},
|
|
199
|
+
getOwnPropertyDescriptor(target, key) {
|
|
200
|
+
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
// writes
|
|
204
|
+
// Writes one key on a raw node and pokes what changed: the key's own signal, the
|
|
205
|
+
// array length signal when the length moved, and the iteration signal.
|
|
206
|
+
/**
|
|
207
|
+
* The raw form of a value on its way into the state.
|
|
208
|
+
*
|
|
209
|
+
* `set({ items: [...cart.items, one] })` spreads proxies back in, and a store that holds its
|
|
210
|
+
* own proxies tracks each of them twice: a second node store per object, a proxy wrapping a
|
|
211
|
+
* proxy, and a graph with two signals for every key.
|
|
212
|
+
*/
|
|
213
|
+
function rawValue(value, seen = new Set()) {
|
|
214
|
+
if (value == null || typeof value !== 'object')
|
|
215
|
+
return value;
|
|
216
|
+
const raw = value[$RAW] ?? value;
|
|
217
|
+
if (seen.has(raw) || !isTrackable(raw))
|
|
218
|
+
return raw;
|
|
219
|
+
seen.add(raw);
|
|
220
|
+
let changed = false;
|
|
221
|
+
const swap = (v) => {
|
|
222
|
+
const next = rawValue(v, seen);
|
|
223
|
+
if (next !== v)
|
|
224
|
+
changed = true;
|
|
225
|
+
return next;
|
|
226
|
+
};
|
|
227
|
+
if (Array.isArray(raw)) {
|
|
228
|
+
const out = raw.map(swap);
|
|
229
|
+
return changed ? out : raw;
|
|
230
|
+
}
|
|
231
|
+
const out = {};
|
|
232
|
+
for (const k of Object.keys(raw))
|
|
233
|
+
out[k] = swap(raw[k]);
|
|
234
|
+
return changed ? out : raw;
|
|
235
|
+
}
|
|
236
|
+
function writeKey(state, key, rawInput) {
|
|
237
|
+
const value = rawValue(rawInput);
|
|
238
|
+
const prev = state[key];
|
|
239
|
+
if (prev === value && (value !== undefined || key in state))
|
|
240
|
+
return;
|
|
241
|
+
const isArray = Array.isArray(state);
|
|
242
|
+
const len = isArray ? state.length : 0;
|
|
243
|
+
if (value === undefined)
|
|
244
|
+
delete state[key];
|
|
245
|
+
else
|
|
246
|
+
state[key] = value;
|
|
247
|
+
const ns = nodesOf(state);
|
|
248
|
+
const node = ns.keys.get(key);
|
|
249
|
+
if (node)
|
|
250
|
+
pokeNode(node, value);
|
|
251
|
+
forgetReplaced(prev, state);
|
|
252
|
+
if (isArray && state.length !== len) {
|
|
253
|
+
const lengthNode = ns.keys.get('length');
|
|
254
|
+
if (lengthNode)
|
|
255
|
+
pokeNode(lengthNode, state.length);
|
|
256
|
+
}
|
|
257
|
+
if (ns.all)
|
|
258
|
+
ns.all.$(undefined);
|
|
259
|
+
}
|
|
260
|
+
function nextValue(value, prev) {
|
|
261
|
+
return typeof value === 'function' ? value(prev) : value;
|
|
262
|
+
}
|
|
263
|
+
// Per-key merge of a partial object; recurses into plain objects, replaces the rest.
|
|
264
|
+
function mergeInto(state, value) {
|
|
265
|
+
for (const key of Object.keys(value)) {
|
|
266
|
+
const next = value[key];
|
|
267
|
+
const prev = state[key];
|
|
268
|
+
if (next &&
|
|
269
|
+
typeof next === 'object' &&
|
|
270
|
+
!Array.isArray(next) &&
|
|
271
|
+
prev &&
|
|
272
|
+
typeof prev === 'object' &&
|
|
273
|
+
!Array.isArray(prev)) {
|
|
274
|
+
mergeInto(prev, next);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
writeKey(state, key, next);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// "a.b[0].c" -> ["a","b","0","c"], "[*].done" -> ["*","done"], "[0][1]" -> ["0","1"].
|
|
282
|
+
function parsePath(path) {
|
|
283
|
+
const out = [];
|
|
284
|
+
const normalized = path.replace(/\[([^\]]*)\]/g, '.$1');
|
|
285
|
+
for (const part of normalized.split('.'))
|
|
286
|
+
if (part.length)
|
|
287
|
+
out.push(part);
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
function applyAt(current, segs, value) {
|
|
291
|
+
const last = segs.length - 1;
|
|
292
|
+
for (let i = 0; i < last; i++) {
|
|
293
|
+
const seg = segs[i];
|
|
294
|
+
if (seg === '*') {
|
|
295
|
+
const rest = segs.slice(i + 1);
|
|
296
|
+
if (Array.isArray(current))
|
|
297
|
+
for (let idx = 0; idx < current.length; idx++)
|
|
298
|
+
applyAt(current[idx], rest, value);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
let next = current[seg];
|
|
302
|
+
if (next == null || typeof next !== 'object') {
|
|
303
|
+
next = /^\d+$/.test(segs[i + 1]) ? [] : {};
|
|
304
|
+
writeKey(current, seg, next);
|
|
305
|
+
}
|
|
306
|
+
current = next;
|
|
307
|
+
}
|
|
308
|
+
const term = segs[last];
|
|
309
|
+
if (term === '*') {
|
|
310
|
+
if (Array.isArray(current))
|
|
311
|
+
for (let idx = 0; idx < current.length; idx++)
|
|
312
|
+
writeKey(current, idx, nextValue(value, current[idx]));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
writeKey(current, term, nextValue(value, current[term]));
|
|
316
|
+
}
|
|
317
|
+
function applyPath(root, path, value) {
|
|
318
|
+
applyAt(root, parsePath(path), value);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Whether a lone array argument is a batch of `[path, value]` pairs.
|
|
322
|
+
*
|
|
323
|
+
* The batch form and a replacement array are the same shape at the call site, so the
|
|
324
|
+
* entries decide. Every one has to be a pair with a string path; anything else is a
|
|
325
|
+
* value. An empty array is not a batch, which is what made `set([])` on an array root
|
|
326
|
+
* do nothing at all.
|
|
327
|
+
*/
|
|
328
|
+
function isPathBatch(value) {
|
|
329
|
+
return (Array.isArray(value) &&
|
|
330
|
+
value.length > 0 &&
|
|
331
|
+
value.every((e) => Array.isArray(e) && e.length >= 2 && typeof e[0] === 'string'));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Replace an array's contents in place, keeping its identity.
|
|
335
|
+
*
|
|
336
|
+
* The tail is written away before the length moves, so an index that had a signal is
|
|
337
|
+
* told it is gone. Assigning `length` alone truncates without notifying anyone, which
|
|
338
|
+
* leaves a stale read behind on every removed index.
|
|
339
|
+
*/
|
|
340
|
+
function replaceArray(target, next) {
|
|
341
|
+
// Keys are strings everywhere else, because every other path reaches writeKey through
|
|
342
|
+
// parsePath. A numeric key here would miss the node the proxy registered under '2'.
|
|
343
|
+
for (let i = target.length - 1; i >= next.length; i--)
|
|
344
|
+
writeKey(target, String(i), undefined);
|
|
345
|
+
if (target.length !== next.length)
|
|
346
|
+
writeKey(target, 'length', next.length);
|
|
347
|
+
for (let i = 0; i < next.length; i++)
|
|
348
|
+
writeKey(target, String(i), next[i]);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Runs the setter overloads against the raw root:
|
|
352
|
+
* set(state => newState) whole-state updater, merged back
|
|
353
|
+
* set(partialObject) per-key merge
|
|
354
|
+
* set([[path, valOrFn], ...]) batch of path updates
|
|
355
|
+
* set(path, ...steps, val) a step per argument: key, index, [indices],
|
|
356
|
+
* { from, to, by }, a predicate, or '*'
|
|
357
|
+
* set(path, val | updater) the common form (dotted, [n], [*])
|
|
358
|
+
*/
|
|
359
|
+
function applySet(root, args) {
|
|
360
|
+
const first = args[0];
|
|
361
|
+
if (typeof first === 'function' && args.length === 1) {
|
|
362
|
+
const next = first(root);
|
|
363
|
+
if (next && next !== root)
|
|
364
|
+
mergeInto(root, next);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (Array.isArray(first) && args.length === 1) {
|
|
368
|
+
if (isPathBatch(first)) {
|
|
369
|
+
for (const entry of first)
|
|
370
|
+
applyPath(root, entry[0], entry[1]);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Not a batch, so it is a value. Against an array root that is a replacement.
|
|
374
|
+
if (Array.isArray(root)) {
|
|
375
|
+
replaceArray(root, first);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// An empty array asks for no updates, which is harmless either way.
|
|
379
|
+
if (first.length === 0)
|
|
380
|
+
return;
|
|
381
|
+
throw new TypeError('[fluixi] set() was given an array that is neither a batch of [path, value] ' +
|
|
382
|
+
'pairs nor a replacement for an array root. Name the path you meant, as in ' +
|
|
383
|
+
"set('items', [...]).");
|
|
384
|
+
}
|
|
385
|
+
if (first &&
|
|
386
|
+
typeof first === 'object' &&
|
|
387
|
+
!Array.isArray(first) &&
|
|
388
|
+
args.length === 1) {
|
|
389
|
+
mergeInto(root, first);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Three or more arguments: every one but the last selects a step, the last is the
|
|
393
|
+
// value. This used to read `args[1]` as the value and ignore the rest, so
|
|
394
|
+
// `set('items', 0, 'done', true)` wrote the number 0 over `items`, accepted by the
|
|
395
|
+
// typings, and silent.
|
|
396
|
+
if (args.length >= 3) {
|
|
397
|
+
setDeep(root, args.slice(0, -1), args[args.length - 1]);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
applyPath(root, String(first), args[1]);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Which keys a selector names at this level.
|
|
404
|
+
*
|
|
405
|
+
* The typings offer four shapes for the steps between a path and a value, and each one
|
|
406
|
+
* resolves here rather than at a call site:
|
|
407
|
+
* 'name' | 2 a single key, dotted paths expanded by the caller
|
|
408
|
+
* [0, 2] several indices of an array
|
|
409
|
+
* { from, to, by } a range of indices
|
|
410
|
+
* (item, i) => bool a predicate over an array
|
|
411
|
+
* '*' every key
|
|
412
|
+
*/
|
|
413
|
+
function selectKeys(current, selector) {
|
|
414
|
+
if (typeof selector === 'function') {
|
|
415
|
+
if (!Array.isArray(current))
|
|
416
|
+
return [];
|
|
417
|
+
const keys = [];
|
|
418
|
+
for (let i = 0; i < current.length; i++)
|
|
419
|
+
if (selector(current[i], i))
|
|
420
|
+
keys.push(i);
|
|
421
|
+
return keys;
|
|
422
|
+
}
|
|
423
|
+
if (Array.isArray(selector)) {
|
|
424
|
+
// Against an array these are indices. Against anything else there is nothing to
|
|
425
|
+
// index, so they are path segments: the form this function used to be given.
|
|
426
|
+
return Array.isArray(current) ? selector.map(Number) : selector.map(String);
|
|
427
|
+
}
|
|
428
|
+
if (selector &&
|
|
429
|
+
typeof selector === 'object' &&
|
|
430
|
+
('from' in selector || 'to' in selector || 'by' in selector)) {
|
|
431
|
+
if (!Array.isArray(current))
|
|
432
|
+
return [];
|
|
433
|
+
const by = selector.by ?? 1;
|
|
434
|
+
const from = selector.from ?? 0;
|
|
435
|
+
const to = selector.to ?? current.length - 1;
|
|
436
|
+
const keys = [];
|
|
437
|
+
for (let i = from; i <= to; i += by)
|
|
438
|
+
if (i >= 0 && i < current.length)
|
|
439
|
+
keys.push(i);
|
|
440
|
+
return keys;
|
|
441
|
+
}
|
|
442
|
+
if (selector === '*') {
|
|
443
|
+
return Array.isArray(current)
|
|
444
|
+
? current.map((_, i) => i)
|
|
445
|
+
: Object.keys(current ?? {});
|
|
446
|
+
}
|
|
447
|
+
return [selector];
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Walk the selectors, writing the value at every leaf they reach.
|
|
451
|
+
*
|
|
452
|
+
* A string step is expanded first, so `set('a.b', 'c', v)` and `set('a', 'b', 'c', v)`
|
|
453
|
+
* land in the same place. Missing containers are created on the way down, as an array
|
|
454
|
+
* when the next step looks like an index.
|
|
455
|
+
*/
|
|
456
|
+
function setDeep(current, selectors, value) {
|
|
457
|
+
if (current == null)
|
|
458
|
+
return;
|
|
459
|
+
// Expand a dotted or bracketed string into its own steps before selecting.
|
|
460
|
+
const [head, ...rest] = selectors;
|
|
461
|
+
if (typeof head === 'string') {
|
|
462
|
+
const parts = parsePath(head);
|
|
463
|
+
if (parts.length > 1) {
|
|
464
|
+
setDeep(current, [...parts, ...rest], value);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const keys = selectKeys(current, head);
|
|
469
|
+
if (rest.length === 0) {
|
|
470
|
+
for (const key of keys)
|
|
471
|
+
writeKey(current, key, nextValue(value, current[key]));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const key of keys) {
|
|
475
|
+
let next = current[key];
|
|
476
|
+
if (next == null || typeof next !== 'object') {
|
|
477
|
+
const lookahead = rest[0];
|
|
478
|
+
next = typeof lookahead === 'number' || /^\d+$/.test(String(lookahead)) ? [] : {};
|
|
479
|
+
writeKey(current, key, next);
|
|
480
|
+
}
|
|
481
|
+
setDeep(next, rest, value);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function clone(value) {
|
|
485
|
+
if (value == null || typeof value !== 'object')
|
|
486
|
+
return value;
|
|
487
|
+
if (Array.isArray(value))
|
|
488
|
+
return value.map(clone);
|
|
489
|
+
const out = {};
|
|
490
|
+
for (const key of Object.keys(value))
|
|
491
|
+
out[key] = clone(value[key]);
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
494
|
+
// Walks the raw tree along a path without tracking.
|
|
495
|
+
function rawAt(root, path) {
|
|
496
|
+
let cur = root;
|
|
497
|
+
for (const seg of parsePath(path)) {
|
|
498
|
+
if (cur == null)
|
|
499
|
+
return undefined;
|
|
500
|
+
cur = cur[seg];
|
|
501
|
+
}
|
|
502
|
+
return cur;
|
|
503
|
+
}
|
|
504
|
+
// store
|
|
505
|
+
// Thin holder around the raw root and its proxy.
|
|
506
|
+
class Store {
|
|
507
|
+
options;
|
|
508
|
+
root;
|
|
509
|
+
set;
|
|
510
|
+
constructor(initialState, options = { immutable: true }) {
|
|
511
|
+
this.options = options;
|
|
512
|
+
this.root = initialState;
|
|
513
|
+
this.set = ((...args) => batch(() => applySet(this.root, args)));
|
|
514
|
+
if (initialState && typeof initialState === 'object') {
|
|
515
|
+
OWNERS.set(initialState, { store: this, name: options.name ?? 'store' });
|
|
516
|
+
ROOTS.set(initialState, initialState);
|
|
517
|
+
}
|
|
518
|
+
// One node for the store itself, so the line that declared it is in the graph even
|
|
519
|
+
// though its key signals are not made until something reads them.
|
|
520
|
+
reportReactiveNode({
|
|
521
|
+
kind: 'store',
|
|
522
|
+
node: this,
|
|
523
|
+
handle: () => this.root,
|
|
524
|
+
name: options.name,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
get state() {
|
|
528
|
+
return box(this.root);
|
|
529
|
+
}
|
|
530
|
+
get proxy() {
|
|
531
|
+
return box(this.root);
|
|
532
|
+
}
|
|
533
|
+
get accessor() {
|
|
534
|
+
return (() => box(this.root));
|
|
535
|
+
}
|
|
536
|
+
unwrap() {
|
|
537
|
+
return this.root;
|
|
538
|
+
}
|
|
539
|
+
getNode(key) {
|
|
540
|
+
return this.root && this.root[key];
|
|
541
|
+
}
|
|
542
|
+
// Tracked read at a path. Empty path returns the whole (unwrapped) state.
|
|
543
|
+
get(path) {
|
|
544
|
+
if (path == null || path === '')
|
|
545
|
+
return unwrap(this.state);
|
|
546
|
+
let cur = this.state;
|
|
547
|
+
for (const seg of parsePath(path)) {
|
|
548
|
+
if (cur == null)
|
|
549
|
+
return undefined;
|
|
550
|
+
cur = cur[seg];
|
|
551
|
+
}
|
|
552
|
+
return unwrap(cur);
|
|
553
|
+
}
|
|
554
|
+
select(selector) {
|
|
555
|
+
return createMemo(() => selector(this.state));
|
|
556
|
+
}
|
|
557
|
+
subscribe(listener) {
|
|
558
|
+
return createEffect(() => listener(this.state));
|
|
559
|
+
}
|
|
560
|
+
watch(selector, effect) {
|
|
561
|
+
let prev;
|
|
562
|
+
return createEffect(() => {
|
|
563
|
+
const cur = selector(this.state);
|
|
564
|
+
if (!Object.is(cur, prev)) {
|
|
565
|
+
effect(cur, prev);
|
|
566
|
+
prev = cur;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
// Reactive accessor for a top-level key.
|
|
571
|
+
reactive(key) {
|
|
572
|
+
return createMemo(() => this.state[key]);
|
|
573
|
+
}
|
|
574
|
+
produce(producer) {
|
|
575
|
+
this.set(((prev) => {
|
|
576
|
+
const draft = clone(prev);
|
|
577
|
+
producer(draft);
|
|
578
|
+
return draft;
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
reconcile(value, _options = {}) {
|
|
582
|
+
this.set(value);
|
|
583
|
+
}
|
|
584
|
+
push(path, ...items) {
|
|
585
|
+
const arr = rawAt(this.root, path);
|
|
586
|
+
this.set(path, arr.concat(items));
|
|
587
|
+
}
|
|
588
|
+
pop(path) {
|
|
589
|
+
const arr = rawAt(this.root, path).slice();
|
|
590
|
+
const item = arr.pop();
|
|
591
|
+
this.set(path, arr);
|
|
592
|
+
return item;
|
|
593
|
+
}
|
|
594
|
+
shift(path) {
|
|
595
|
+
const arr = rawAt(this.root, path).slice();
|
|
596
|
+
const item = arr.shift();
|
|
597
|
+
this.set(path, arr);
|
|
598
|
+
return item;
|
|
599
|
+
}
|
|
600
|
+
splice(path, start, deleteCount, ...items) {
|
|
601
|
+
const arr = rawAt(this.root, path).slice();
|
|
602
|
+
const removed = arr.splice(start, deleteCount, ...items);
|
|
603
|
+
this.set(path, arr);
|
|
604
|
+
return removed;
|
|
605
|
+
}
|
|
606
|
+
insert(path, index, value) {
|
|
607
|
+
const arr = rawAt(this.root, path).slice();
|
|
608
|
+
arr.splice(index, 0, value);
|
|
609
|
+
this.set(path, arr);
|
|
610
|
+
}
|
|
611
|
+
removeAt(path, index) {
|
|
612
|
+
const arr = rawAt(this.root, path).slice();
|
|
613
|
+
const removed = arr.splice(index, 1);
|
|
614
|
+
this.set(path, arr);
|
|
615
|
+
return removed;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Swap a whole value, rather than merging into it.
|
|
619
|
+
*
|
|
620
|
+
* `set(partial)` merges per key, which is what you want for an object and never what
|
|
621
|
+
* you want for an array. This replaces: `replace(value)` at the root, or
|
|
622
|
+
* `replace(path, value)` further down.
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* ```ts
|
|
626
|
+
* const [items, set] = createStore([1, 2, 3]);
|
|
627
|
+
* set.replace([]); // now empty
|
|
628
|
+
* set.replace(['a', 'b']); // now two entries
|
|
629
|
+
* ```
|
|
630
|
+
*/
|
|
631
|
+
replace(pathOrValue, maybeValue) {
|
|
632
|
+
if (arguments.length >= 2) {
|
|
633
|
+
const target = rawAt(this.root, String(pathOrValue));
|
|
634
|
+
const next = maybeValue;
|
|
635
|
+
if (Array.isArray(target) && Array.isArray(next)) {
|
|
636
|
+
replaceArray(target, next);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
this.set(pathOrValue, next);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const root = this.root;
|
|
643
|
+
if (Array.isArray(root) && Array.isArray(pathOrValue)) {
|
|
644
|
+
replaceArray(root, pathOrValue);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
this.set(pathOrValue);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Empty an array, at the root or at a path.
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* ```ts
|
|
654
|
+
* set.clear(); // the root array
|
|
655
|
+
* set.clear('todos'); // a nested one
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
clear(path) {
|
|
659
|
+
const target = path === undefined ? this.root : rawAt(this.root, path);
|
|
660
|
+
if (Array.isArray(target)) {
|
|
661
|
+
replaceArray(target, []);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (target && typeof target === 'object') {
|
|
665
|
+
for (const key of Object.keys(target))
|
|
666
|
+
writeKey(target, key, undefined);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
toObservable() {
|
|
670
|
+
const [get] = createSignal(this.state);
|
|
671
|
+
return get.toObservable();
|
|
672
|
+
}
|
|
673
|
+
dispose() { }
|
|
674
|
+
}
|
|
675
|
+
// The path/array/derivation helpers, attached to the returned setter (as the old
|
|
676
|
+
// API exposed them). Kept off the proxy since the proxy is read-only.
|
|
677
|
+
const SETTER_METHODS = [
|
|
678
|
+
'get',
|
|
679
|
+
'select',
|
|
680
|
+
'watch',
|
|
681
|
+
'subscribe',
|
|
682
|
+
'produce',
|
|
683
|
+
'reconcile',
|
|
684
|
+
'push',
|
|
685
|
+
'pop',
|
|
686
|
+
'shift',
|
|
687
|
+
'splice',
|
|
688
|
+
'insert',
|
|
689
|
+
'removeAt',
|
|
690
|
+
'replace',
|
|
691
|
+
'clear',
|
|
692
|
+
'unwrap',
|
|
693
|
+
'toObservable',
|
|
694
|
+
];
|
|
695
|
+
function attachStoreMethods(target, store) {
|
|
696
|
+
for (const name of SETTER_METHODS)
|
|
697
|
+
target[name] = store[name].bind(store);
|
|
698
|
+
target.$ = store.reactive.bind(store);
|
|
699
|
+
}
|
|
700
|
+
// public api
|
|
701
|
+
/**
|
|
702
|
+
* A fine-grained reactive store: a plain object tree that tracks reads per key.
|
|
703
|
+
*
|
|
704
|
+
* Returns the state and its setter as a tuple. The state is a proxy, so reading a
|
|
705
|
+
* property subscribes the current computation to that property alone, and a write
|
|
706
|
+
* elsewhere in the tree does not disturb it. Signals are created lazily, one per key
|
|
707
|
+
* that something actually reads, so the cost follows what the app looks at rather than
|
|
708
|
+
* the size of the state.
|
|
709
|
+
*
|
|
710
|
+
* The state is immutable from the outside by default, assigning to it does nothing, and
|
|
711
|
+
* `set` is the way in. That is what makes the tracking trustworthy, since every change
|
|
712
|
+
* goes through a path the store can see.
|
|
713
|
+
*
|
|
714
|
+
* `store()` wraps this as a single binding for the common case. Prefer it unless the
|
|
715
|
+
* state has a `set` key of its own, which the single-binding form cannot express.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```ts
|
|
719
|
+
* const [state, setState] = createStore({ user: { name: 'Ada' }, count: 0 });
|
|
720
|
+
*
|
|
721
|
+
* effect(() => console.log(state.user.name)); // tracks user.name only
|
|
722
|
+
* setState('count', 1); // the effect does not re-run
|
|
723
|
+
* setState('user.name', 'Grace'); // now it does
|
|
724
|
+
* ```
|
|
725
|
+
*
|
|
726
|
+
* @example A patch merges the keys it names; a dot path reaches through arrays:
|
|
727
|
+
* ```ts
|
|
728
|
+
* setState({ count: 5 }); // user is untouched
|
|
729
|
+
* setState('todos.0.done', true);
|
|
730
|
+
* setState('count', (n) => n + 1); // or derive from the current value
|
|
731
|
+
* ```
|
|
732
|
+
*/
|
|
733
|
+
function createStore(initialState, options = { immutable: true }) {
|
|
734
|
+
const store = new Store(initialState, options);
|
|
735
|
+
attachStoreMethods(store.set, store);
|
|
736
|
+
return [store.proxy, store.set];
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* A store you assign to directly, without a setter.
|
|
740
|
+
*
|
|
741
|
+
* The same per-key tracking as `createStore`, with the immutability dropped, so
|
|
742
|
+
* `state.count = 1` is a tracked write. Reach for it when the state is local to one
|
|
743
|
+
* component and the indirection of a setter buys nothing. The cost is that a change
|
|
744
|
+
* can come from anywhere holding the object.
|
|
745
|
+
*
|
|
746
|
+
* @example
|
|
747
|
+
* ```ts
|
|
748
|
+
* const state = createMutableStore({ count: 0 });
|
|
749
|
+
* effect(() => console.log(state.count));
|
|
750
|
+
* state.count = 1; // the effect re-runs
|
|
751
|
+
* ```
|
|
752
|
+
*/
|
|
753
|
+
function createMutableStore(initialState, options = {}) {
|
|
754
|
+
return new Store(initialState, { ...options, immutable: false });
|
|
755
|
+
}
|
|
756
|
+
/** A store whose state is derived from a reactive function. */
|
|
757
|
+
function createComputedStore(fn, options = {}) {
|
|
758
|
+
const store = new Store(fn(), options);
|
|
759
|
+
createEffect(() => store.set(fn()));
|
|
760
|
+
return store;
|
|
761
|
+
}
|
|
762
|
+
/** Runs an effect over a slice of store state. */
|
|
763
|
+
function createStoreEffect(state, selector, effect) {
|
|
764
|
+
return createEffect(() => effect(selector(state)));
|
|
765
|
+
}
|
|
766
|
+
/** Memoises a slice of store state. */
|
|
767
|
+
function createStoreMemo(store, selector, options) {
|
|
768
|
+
return createMemo(() => selector(store), undefined, options);
|
|
769
|
+
}
|
|
770
|
+
/** Wraps a value as an accessor carrying the store helper methods. */
|
|
771
|
+
function wrap(value, options = {}) {
|
|
772
|
+
const store = new Store(value, options);
|
|
773
|
+
const accessor = (() => store.proxy);
|
|
774
|
+
attachStoreMethods(accessor, store);
|
|
775
|
+
return accessor;
|
|
776
|
+
}
|
|
777
|
+
function unwrap(item) {
|
|
778
|
+
if (item == null || typeof item !== 'object')
|
|
779
|
+
return item;
|
|
780
|
+
const raw = item[$RAW];
|
|
781
|
+
return raw !== undefined ? raw : item;
|
|
782
|
+
}
|
|
783
|
+
function untrackStore(store) {
|
|
784
|
+
return untrack(() => unwrap(store));
|
|
785
|
+
}
|
|
786
|
+
export function isStore(store) {
|
|
787
|
+
return isStoreProxy(store);
|
|
788
|
+
}
|
|
789
|
+
export function getNode(store) {
|
|
790
|
+
return isStoreProxy(store) ? store[$NODE] : null;
|
|
791
|
+
}
|
|
792
|
+
export function getStoreNode(store) {
|
|
793
|
+
if (store && typeof store === 'object' && $NODE in store)
|
|
794
|
+
return store[$NODE];
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
export function isStoreProxy(value) {
|
|
798
|
+
return (!!value &&
|
|
799
|
+
typeof value === 'object' &&
|
|
800
|
+
($PROXY in value || value.__proxy === true));
|
|
801
|
+
}
|
|
802
|
+
export function modifiable(value) {
|
|
803
|
+
return value;
|
|
804
|
+
}
|
|
805
|
+
export const StoreAPI = Object.freeze({
|
|
806
|
+
createStore,
|
|
807
|
+
createMutableStore,
|
|
808
|
+
batch,
|
|
809
|
+
unwrap,
|
|
810
|
+
untrackStore,
|
|
811
|
+
});
|
|
812
|
+
export { createStore, createMutableStore, createComputedStore, createStoreEffect, createStoreMemo, wrap, unwrap, untrackStore, Store, };
|