sprae 9.1.1 → 10.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/store.js ADDED
@@ -0,0 +1,156 @@
1
+ // signals-based proxy
2
+ import { signal, computed, effect, batch, untracked } from './signal.js'
3
+
4
+ export const _signals = Symbol('signals'), _change = Symbol('length');
5
+
6
+ // object store is not lazy
7
+ export default function store(values, signals) {
8
+ if (!values) return values
9
+
10
+ // ignore existing state as argument
11
+ if (values[_signals] && !signals) return values;
12
+
13
+ // redirect for optimized array store
14
+ if (Array.isArray(values)) return list(values)
15
+
16
+ // ignore non-objects
17
+ if (values.constructor !== Object) return values;
18
+
19
+ // NOTE: if you decide to unlazy values, think about large arrays - init upfront can be costly
20
+ let _len = signal(Object.values(values).length)
21
+
22
+ signals ||= {}
23
+ // proxy conducts prop access to signals
24
+ const state = new Proxy(signals, {
25
+ get: (_, key) => key === _change ? _len : key === _signals ? signals : signals[key]?.valueOf(),
26
+ set: (_, key, v, s) => (s = signals[key], set(signals, key, v), s || (++_len.value)), // bump length for new signal
27
+ deleteProperty: (_, key) => del(signals, key) && _len.value--,
28
+ ownKeys() {
29
+ // subscribe to length when object is spread
30
+ _len.value
31
+ return Reflect.ownKeys(signals);
32
+ },
33
+ })
34
+
35
+ // take over existing store signals instead of creating new ones
36
+ if (values[_signals]) for (let key in values) signals[key] = values[_signals][key];
37
+ else for (let key in values) {
38
+ const desc = Object.getOwnPropertyDescriptor(values, key)
39
+
40
+ // getter turns into computed
41
+ if (desc?.get) {
42
+ // stash setter
43
+ (signals[key] = computed(desc.get.bind(state)))._set = desc.set?.bind(state);
44
+ }
45
+ else {
46
+ // init blank signal - make sure we don't take prototype one
47
+ signals[key] = null
48
+ set(signals, key, values[key]);
49
+ }
50
+ }
51
+
52
+ return state
53
+ }
54
+
55
+
56
+ // array store - signals are lazy since arrays can be very large & expensive
57
+ export function list(values) {
58
+ // track last accessed property to find out if .length was directly accessed from expression or via .push/etc method
59
+ let lastProp
60
+
61
+ // ignore existing state as argument
62
+ if (values[_signals]) return values;
63
+
64
+ // .length signal is stored separately, since it cannot be replaced on array
65
+ let _len = signal(values.length),
66
+ // gotta fill with null since proto methods like .reduce may fail
67
+ signals = Array(values.length).fill(null);
68
+
69
+ // proxy conducts prop access to signals
70
+ const state = new Proxy(signals, {
71
+ get(_, key) {
72
+ if (key === _change) return _len
73
+ if (key === _signals) return signals
74
+
75
+ // console.log('get', key)
76
+ // if .length is read within .push/etc - peek signal to avoid recursive subscription
77
+ if (key === 'length') return (Array.prototype[lastProp]) ? _len.peek() : _len.value;
78
+
79
+ lastProp = key;
80
+
81
+ if (signals[key]) return signals[key].valueOf()
82
+
83
+ // I hope reading values here won't diverge from signals
84
+ if (key < signals.length) return (signals[key] = signal(store(values[key]))).value
85
+ },
86
+
87
+ set(_, key, v) {
88
+ // .length
89
+ if (key === 'length') {
90
+ // force cleaning up tail
91
+ for (let i = v, l = signals.length; i < l; i++) delete state[i]
92
+ _len.value = signals.length = v;
93
+ return true
94
+ }
95
+
96
+ set(signals, key, v)
97
+
98
+ // force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
99
+ if (key >= _len.peek()) _len.value = signals.length = Number(key) + 1
100
+
101
+ return true
102
+ },
103
+
104
+ deleteProperty: (_, key) => (del(signals, key), true),
105
+
106
+ })
107
+
108
+ return state
109
+ }
110
+
111
+ // set/update signal value
112
+ function set(signals, key, v) {
113
+ let s = signals[key]
114
+
115
+ // new property
116
+ if (!s) {
117
+ // preserve signal value as is
118
+ signals[key] = s = v?.peek ? v : signal(store(v))
119
+ }
120
+ // skip unchanged (although can be handled by last condition - we skip a few checks this way)
121
+ else if (v === s.peek());
122
+ // stashed _set for value with getter/setter
123
+ else if (s._set) s._set(v)
124
+ // patch array
125
+ else if (Array.isArray(v) && Array.isArray(s.peek())) {
126
+ const cur = s.peek()
127
+ // if we update plain array (stored in signal) - take over value instead
128
+ if (cur[_change]) untracked(() => {
129
+ batch(() => {
130
+ let i = 0, l = v.length;
131
+ for (; i < l; i++) cur[i] = v[i]
132
+ cur.length = l // forces deleting tail signals
133
+ })
134
+ })
135
+ else {
136
+ s.value = v
137
+ }
138
+ }
139
+ // .x = y
140
+ else {
141
+ s.value = store(v)
142
+ }
143
+ }
144
+
145
+ // delete signal
146
+ function del(signals, key) {
147
+ // console.log('delete', key)
148
+ const s = signals[key]
149
+ if (s) {
150
+ const del = s[Symbol.dispose]
151
+ if (del) delete s[Symbol.dispose]
152
+ delete signals[key]
153
+ del?.()
154
+ return true
155
+ }
156
+ }
@@ -1,10 +0,0 @@
1
- import sprae, { directive } from "../core.js";
2
-
3
- // `:each` can redefine scope as `:each="a in {myScope}"`,
4
- // same time per-item scope as `:each="..." :scope="{collapsed:true}"` is useful
5
- directive.scope = (el, evaluate, rootState) => {
6
- // local state may contain signals that update, so we take them over
7
- return () => {
8
- sprae(el, { ...rootState, ...(evaluate(rootState)?.valueOf?.() || {}) });
9
- }
10
- };