sprae 7.0.0 → 8.0.1

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/src/core.js CHANGED
@@ -1,16 +1,20 @@
1
- import createState, { fx, sandbox, batch } from './state.signals-proxy.js';
2
- import defaultDirective, { primary, secondary, on } from './directives.js';
1
+ import createState, { batch, sandbox, _dispose } from './state.signals-proxy.js';
2
+ import defaultDirective, { primary, secondary } from './directives.js';
3
3
 
4
+
5
+ // default root sandbox
4
6
  sprae.globals = sandbox
5
7
 
6
8
  // sprae element: apply directives
7
9
  const memo = new WeakMap
8
10
  export default function sprae(container, values) {
9
- if (!container.children) return
11
+ if (!container.children) return // ignore what?
12
+
10
13
  if (memo.has(container)) return batch(() => Object.assign(memo.get(container), values))
11
14
 
15
+ // take over existing state instead of creating clone
12
16
  const state = createState(values || {});
13
- const updates = []
17
+ const disposes = []
14
18
 
15
19
  // init directives on element
16
20
  const init = (el, parent = el.parentNode) => {
@@ -21,9 +25,9 @@ export default function sprae(container, values) {
21
25
  let expr = el.getAttribute(attrName)
22
26
  el.removeAttribute(attrName)
23
27
 
24
- updates.push(primary[name](el, expr, state, name))
28
+ disposes.push(primary[name](el, expr, state, name))
25
29
 
26
- // stop if element was spraed by directive or skipped (detached)
30
+ // stop if element was spraed by directive or skipped (detached) like in case of :if or :each
27
31
  if (memo.has(el)) return
28
32
  if (el.parentNode !== parent) return false
29
33
  }
@@ -44,7 +48,7 @@ export default function sprae(container, values) {
44
48
  // @click forwards to :onclick=event=>{...inline}
45
49
  if (prefix === '@') name = `on` + name
46
50
  let dir = secondary[name] || defaultDirective;
47
- updates.push(dir(el, expr, state, name));
51
+ disposes.push(dir(el, expr, state, name));
48
52
  // NOTE: secondary directives don't stop flow nor extend state, so no need to check
49
53
  }
50
54
  }
@@ -60,17 +64,19 @@ export default function sprae(container, values) {
60
64
 
61
65
  init(container);
62
66
 
63
- // call updates: subscribes directives to state;
64
- // state is created after inits because directives can extend init values (expose refs etc)
65
- for (let update of updates) if (update) {
66
- let teardown
67
- fx(() => {
68
- if (typeof teardown === 'function') teardown()
69
- teardown = update(state)
70
- });
71
- }
67
+ // if element was spraed by :with or :each instruction - skip
68
+ if (memo.has(container)) return state //memo.get(container)
72
69
 
70
+ // save
73
71
  memo.set(container, state);
74
72
 
73
+ // expose dispose
74
+ if (disposes.length) Object.defineProperty(container, _dispose, {
75
+ value: () => {
76
+ while (disposes.length) disposes.shift()?.();
77
+ memo.delete(container);
78
+ }
79
+ });
80
+
75
81
  return state;
76
82
  }
package/src/directives.js CHANGED
@@ -1,9 +1,7 @@
1
1
  // directives & parsing
2
2
  import sprae from './core.js'
3
- import swap from './domdiff.js'
4
- // import swap from 'swapdom'
5
- import createState from './state.signals-proxy.js'
6
- import { queueMicrotask, WeakishMap } from './util.js'
3
+ import createState, { effect, computed, untracked, batch, _dispose, _signals } from './state.signals-proxy.js'
4
+ import { queueMicrotask } from './util.js'
7
5
 
8
6
  // reserved directives - order matters!
9
7
  // primary initialized first by selector, secondary initialized by iterating attributes
@@ -12,11 +10,12 @@ export const primary = {}, secondary = {}
12
10
  // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
13
11
  // as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
14
12
  // we consider :with={x} :if={x} case insignificant
15
- primary['if'] = (el, expr) => {
13
+ primary['if'] = (el, expr, state) => {
16
14
  let holder = document.createTextNode(''),
17
15
  clauses = [parseExpr(el, expr, ':if')],
18
16
  els = [el], cur = el
19
17
 
18
+ // consume all following siblings with :else, :if into a single group
20
19
  while (cur = el.nextElementSibling) {
21
20
  if (cur.hasAttribute(':else')) {
22
21
  cur.removeAttribute(':else');
@@ -33,7 +32,8 @@ primary['if'] = (el, expr) => {
33
32
 
34
33
  el.replaceWith(cur = holder)
35
34
 
36
- return (state) => {
35
+ const dispose = effect(() => {
36
+ // find matched clause (re-evaluates any time any clause updates)
37
37
  let i = clauses.findIndex(f => f(state))
38
38
  if (els[i] != cur) {
39
39
  ; (cur[_each] || cur).replaceWith(cur = els[i] || holder);
@@ -41,74 +41,91 @@ primary['if'] = (el, expr) => {
41
41
  // but :if must come first to avoid preliminary caching
42
42
  sprae(cur, state);
43
43
  }
44
+ })
45
+
46
+ return () => {
47
+ for (const el of els) el[_dispose]?.() // dispose all internal spraes
48
+ dispose() // dispose subscription
44
49
  }
45
50
  }
46
51
 
47
52
  const _each = Symbol(':each')
48
53
 
49
54
  // :each must init before :ref, :id or any others, since it defines scope
50
- primary['each'] = (tpl, expr) => {
55
+ primary['each'] = (tpl, expr, state) => {
51
56
  let each = parseForExpression(expr);
52
57
  if (!each) return exprError(new Error, tpl, expr, ':each');
53
58
 
54
- // FIXME: make sure no memory leak here
59
+ const [itemVar, idxVar, itemsExpr] = each;
60
+
55
61
  // we need :if to be able to replace holder instead of tpl for :if :each case
56
62
  const holder = tpl[_each] = document.createTextNode('')
57
63
  tpl.replaceWith(holder)
58
64
 
59
- const evaluate = parseExpr(tpl, each[2], ':each');
60
-
61
- const keyExpr = tpl.getAttribute(':key');
62
- const itemKey = keyExpr ? parseExpr(null, keyExpr, ':each') : null;
63
- tpl.removeAttribute(':key')
64
-
65
- const refExpr = tpl.getAttribute(':ref');
66
-
67
- const scopes = new WeakishMap() // stores scope per data item
68
- const itemEls = new WeakishMap() // element per data item
69
- let curEls = []
70
-
71
- return (state) => {
72
- // get items
73
- let list = evaluate(state)
74
-
75
- if (!list) list = []
76
- else if (typeof list === 'number') list = Array.from({ length: list }, (_, i) => [i + 1, i])
77
- else if (Array.isArray(list)) list = list.map((item, i) => [i + 1, item])
78
- else if (typeof list === 'object') list = Object.entries(list)
79
- else exprError(Error('Bad list value'), tpl, expr, ':each', list)
65
+ const evaluate = parseExpr(tpl, itemsExpr, ':each');
80
66
 
81
- // collect elements/scopes for items
82
- let newEls = [], elScopes = []
67
+ // we re-create items any time new items are produced
68
+ let items, prevl = 0, keys
69
+ effect(() => {
70
+ let newItems = evaluate(state)
83
71
 
84
- for (let [idx, item] of list) {
85
- let el, scope, key = itemKey?.({ [each[0]]: item, [each[1]]: idx })
86
-
87
- // we consider if data items are primitive, then nodes needn't be cached
88
- // since likely they're very simple to create
89
- if (key == null) el = tpl.cloneNode(true);
90
- else (el = itemEls.get(key)) || itemEls.set(key, el = tpl.cloneNode(true));
91
-
92
- newEls.push(el)
93
-
94
- if (key == null || !(scope = scopes.get(key))) {
95
- scope = createState({ [each[0]]: item, [refExpr || '']: null, [each[1]]: idx }, state)
96
- if (key != null) scopes.set(key, scope)
97
- }
98
- // need to explicitly set item to update existing children's values
99
- else scope[each[0]] = item
100
-
101
- elScopes.push(scope)
72
+ // convert items to array
73
+ if (!newItems) newItems = []
74
+ else if (typeof newItems === 'number') {
75
+ newItems = Array.from({ length: newItems }, (_, i) => i)
76
+ }
77
+ else if (Array.isArray(newItems));
78
+ else if (typeof newItems === 'object') {
79
+ keys = Object.keys(newItems);
80
+ newItems = Object.values(newItems);
81
+ }
82
+ else {
83
+ exprError(Error('Bad items value'), tpl, expr, ':each', newItems)
102
84
  }
103
85
 
104
- swap(holder.parentNode, curEls, newEls, holder)
105
- curEls = newEls
86
+ untracked(() => batch(() => {
87
+ // init items
88
+ if (!items?.[_signals][0]?.peek) {
89
+ // manual dispose for plain arrays (not states) - _signals here is just fake holder for destructors
90
+ for (let i = 0, signals = items?.[_signals]; i < prevl; i++) signals[i]?._del()
91
+ // NOTE: new items are initialized in length effect below
92
+ items = newItems, items[_signals] ||= {}
93
+ }
94
+ // patch existing items and insert new items - init happens in length effect
95
+ else {
96
+ let newl = newItems.length, i = 0
97
+ for (; i < newl; i++) items[i] = newItems[i]
98
+ items.length = newl // dispose tail (done internally in state)
99
+ }
100
+ }))
101
+
102
+ // length change effect
103
+ return effect(() => {
104
+ let newl = newItems.length // indicate that we track it
105
+ if (prevl !== newl) untracked(() => batch(() => {
106
+ // init new items
107
+ const signals = items[_signals]
108
+ for (let i = prevl; i < newl; i++) {
109
+ items[i]; // touch item to create signal
110
+ const el = tpl.cloneNode(true),
111
+ scope = createState({
112
+ [itemVar]: signals[i] ?? items[i],
113
+ [idxVar]: keys?.[i] ?? i
114
+ }, state)
115
+ holder.before(el)
116
+ sprae(el, scope)
117
+ const { _del } = (signals[i] ||= {});
118
+ signals[i]._del = () => { delete signals[i]; _del?.(); el[_dispose](), el.remove(), delete items[i] }
119
+ }
120
+ prevl = newl
121
+ }))
122
+ })
123
+ })
106
124
 
107
- // init new elements
108
- for (let i = 0; i < newEls.length; i++) {
109
- sprae(newEls[i], elScopes[i])
110
- }
111
- }
125
+ return () => batch(() => {
126
+ for (let _i of items[_signals]) _i?._del()
127
+ items.length = 0
128
+ })
112
129
  }
113
130
 
114
131
  // `:each` can redefine scope as `:each="a in {myScope}"`,
@@ -117,7 +134,8 @@ primary['with'] = (el, expr, rootState) => {
117
134
  let evaluate = parseExpr(el, expr, ':with')
118
135
  const localState = evaluate(rootState)
119
136
  let state = createState(localState, rootState)
120
- sprae(el, state);
137
+ sprae(el, state)
138
+ return el[_dispose];
121
139
  }
122
140
 
123
141
  // ref must be last within primaries, since that must be skipped by :each, but before secondaries
@@ -148,7 +166,6 @@ function parseForExpression(expression) {
148
166
  items
149
167
  ]
150
168
 
151
- // FIXME: it can possibly return index as second param
152
169
  return [item, '', items]
153
170
  }
154
171
 
@@ -161,18 +178,19 @@ secondary['render'] = (el, expr, state) => {
161
178
  let content = tpl.content.cloneNode(true);
162
179
  el.replaceChildren(content)
163
180
  sprae(el, state)
181
+ return el[_dispose]
164
182
  }
165
183
 
166
- secondary['id'] = (el, expr) => {
184
+ secondary['id'] = (el, expr, state) => {
167
185
  let evaluate = parseExpr(el, expr, ':id')
168
186
  const update = v => el.id = v || v === 0 ? v : ''
169
- return (state) => update(evaluate(state))
187
+ return effect(() => update(evaluate(state)))
170
188
  }
171
189
 
172
- secondary['class'] = (el, expr) => {
190
+ secondary['class'] = (el, expr, state) => {
173
191
  let evaluate = parseExpr(el, expr, ':class')
174
192
  let initClassName = el.getAttribute('class')
175
- return (state) => {
193
+ return effect(() => {
176
194
  let v = evaluate(state)
177
195
  let className = [initClassName]
178
196
  if (v) {
@@ -182,42 +200,44 @@ secondary['class'] = (el, expr) => {
182
200
  }
183
201
  if (className = className.filter(Boolean).join(' ')) el.setAttribute('class', className);
184
202
  else el.removeAttribute('class')
185
- }
203
+ })
186
204
  }
187
205
 
188
- secondary['style'] = (el, expr) => {
206
+ secondary['style'] = (el, expr, state) => {
189
207
  let evaluate = parseExpr(el, expr, ':style')
190
208
  let initStyle = el.getAttribute('style') || ''
191
209
  if (!initStyle.endsWith(';')) initStyle += '; '
192
- return (state) => {
210
+ return effect(() => {
193
211
  let v = evaluate(state)
194
212
  if (typeof v === 'string') el.setAttribute('style', initStyle + v)
195
213
  else {
196
- el.setAttribute('style', initStyle)
197
- for (let k in v) el.style.setProperty(k, v[k])
214
+ untracked(() => {
215
+ el.setAttribute('style', initStyle)
216
+ for (let k in v) if (typeof v[k] !== 'symbol') el.style.setProperty(k, v[k])
217
+ })
198
218
  }
199
- }
219
+ })
200
220
  }
201
221
 
202
- secondary['text'] = (el, expr) => {
222
+ secondary['text'] = (el, expr, state) => {
203
223
  let evaluate = parseExpr(el, expr, ':text')
204
- return (state) => {
224
+ return effect(() => {
205
225
  let value = evaluate(state)
206
226
  el.textContent = value == null ? '' : value;
207
- }
227
+ })
208
228
  }
209
229
 
210
230
  // set props in-bulk or run effect
211
- secondary[''] = (el, expr) => {
231
+ secondary[''] = (el, expr, state) => {
212
232
  let evaluate = parseExpr(el, expr, ':')
213
- if (evaluate) return (state) => {
233
+ if (evaluate) return effect(() => {
214
234
  let value = evaluate(state)
215
235
  for (let key in value) attr(el, dashcase(key), value[key]);
216
- }
236
+ })
217
237
  }
218
238
 
219
239
  // connect expr to element value
220
- secondary['value'] = (el, expr) => {
240
+ secondary['value'] = (el, expr, state) => {
221
241
  let evaluate = parseExpr(el, expr, ':value')
222
242
 
223
243
  let from, to
@@ -238,7 +258,7 @@ secondary['value'] = (el, expr) => {
238
258
  value => el.value = value
239
259
  )
240
260
 
241
- return (state) => update(evaluate(state))
261
+ return effect(() => { update(evaluate(state)) })
242
262
  }
243
263
 
244
264
  // any unknown directive
@@ -248,17 +268,21 @@ export default (el, expr, state, name) => {
248
268
 
249
269
  if (!evaluate) return
250
270
 
251
- if (evt) return (state => {
252
- // we need anonymous callback to enable modifiers like prevent
253
- let value = evaluate(state) || (() => { })
254
- return on(el, evt, value)
255
- })
271
+ if (evt) {
272
+ let off, dispose = effect(() => {
273
+ if (off) off(), off = null
274
+ // we need anonymous callback to enable modifiers like prevent
275
+ let value = evaluate(state)
276
+ if (value) off = on(el, evt, value)
277
+ })
278
+ return () => (off?.(), dispose())
279
+ }
256
280
 
257
- return state => attr(el, name, evaluate(state))
281
+ return effect(() => { attr(el, name, evaluate(state)) })
258
282
  }
259
283
 
260
284
  // bind event to a target
261
- export const on = (el, e, fn) => {
285
+ const on = (el, e, fn) => {
262
286
  if (!fn) return
263
287
 
264
288
  const ctx = { evt: '', target: el, test: () => true };
@@ -280,7 +304,9 @@ export const on = (el, e, fn) => {
280
304
  )
281
305
 
282
306
  target.addEventListener(evt, cb, opts)
283
- return () => target.removeEventListener(evt, cb, opts)
307
+
308
+ // return off
309
+ return () => (target.removeEventListener(evt, cb, opts))
284
310
  }
285
311
 
286
312
  // event modifiers
@@ -386,7 +412,7 @@ function parseExpr(el, expression, dir) {
386
412
 
387
413
  if (!evaluate) {
388
414
  try {
389
- evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { return ${expression.trim()} };`)
415
+ evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { let __; return ${expression.trim()} };`)
390
416
  } catch (e) {
391
417
  return exprError(e, el, expression, dir)
392
418
  }
package/src/domdiff.js CHANGED
@@ -1,23 +1,26 @@
1
1
  // https://github.com/luwes/js-diff-benchmark/blob/master/libs/list-difference.js
2
2
  // this implementation is more persistent in terms of preserving in-between nodes
3
+ // also it handles _dispose
3
4
 
4
5
  // a is old list, b is the new
5
- export default function(parent, a, b, before) {
6
+ export default function (parent, a, b, before) {
6
7
  const aIdx = new Map();
7
8
  const bIdx = new Map();
8
9
  let i;
9
10
  let j;
10
11
 
11
- // Create a mapping from keys to their position in the old list
12
- for (i = 0; i < a.length; i++) {
13
- aIdx.set(a[i], i);
14
- }
15
-
16
12
  // Create a mapping from keys to their position in the new list
17
13
  for (i = 0; i < b.length; i++) {
18
14
  bIdx.set(b[i], i);
19
15
  }
20
16
 
17
+ // Create a mapping from keys to their position in the old list
18
+ for (i = 0; i < a.length; i++) {
19
+ aIdx.set(a[i], i);
20
+ // dispose a[i] if is going to disappear
21
+ if (!bIdx.has(a[i])) a[i][Symbol.dispose]?.()
22
+ }
23
+
21
24
  for (i = j = 0; i !== a.length || j !== b.length;) {
22
25
  var aElm = a[i], bElm = b[j];
23
26
  if (aElm === null) {
@@ -65,4 +68,4 @@ export default function(parent, a, b, before) {
65
68
  }
66
69
  }
67
70
  return b;
68
- };
71
+ };
@@ -122,7 +122,7 @@ export default function state(obj, parent) {
122
122
  return proxy
123
123
  }
124
124
 
125
- export const fx = (fn) => {
125
+ const fx = (fn) => {
126
126
  const call = () => {
127
127
  let prev = currentFx
128
128
  currentFx = call
@@ -136,7 +136,7 @@ export const fx = (fn) => {
136
136
  return call
137
137
  }
138
138
 
139
- export const planUpdate = () => {
139
+ const planUpdate = () => {
140
140
  // if (!pendingUpdate) {
141
141
  // pendingUpdate = true
142
142
  // queueMicrotask(() => {
@@ -146,3 +146,5 @@ export const planUpdate = () => {
146
146
  // })
147
147
  // }
148
148
  }
149
+
150
+ export { fx as effect };
@@ -7,11 +7,15 @@
7
7
  // + it's just robust
8
8
  // ? must it modify initial store
9
9
 
10
- import { signal, computed, effect, batch } from '@preact/signals-core'
10
+ import { signal, computed, effect, batch, untracked } from '@preact/signals-core'
11
11
  // import { signal, computed } from 'usignal/sync'
12
12
  // import { signal, computed } from '@webreflection/signal'
13
13
 
14
- export { effect as fx, batch }
14
+ export { effect, computed, batch, untracked }
15
+
16
+ export const _dispose = (Symbol.dispose ||= Symbol('dispose'));
17
+ export const _signals = Symbol('signals');
18
+
15
19
 
16
20
  // default root sandbox
17
21
  export const sandbox = {
@@ -22,59 +26,87 @@ export const sandbox = {
22
26
  }
23
27
 
24
28
  const isObject = v => v?.constructor === Object
25
- const memo = new WeakMap
29
+ const isPrimitive = (value) => value !== Object(value);
26
30
 
27
31
  // track last accessed property to figure out if .length was directly accessed from expression or via .push/etc method
28
32
  let lastProp
29
33
 
30
34
  export default function createState(values, parent) {
31
35
  if (!isObject(values) && !Array.isArray(values)) return values;
32
- if (memo.has(values) && !parent) return values;
36
+ // ignore existing state as argument
37
+ if (values[_signals] && !parent) return values;
38
+ const initSignals = values[_signals]
39
+
33
40
  // .length signal is stored outside, since cannot be replaced
34
- const _len = Array.isArray(values) && signal(values.length),
41
+ const _len = Array.isArray(values) && signal((initSignals || values).length),
35
42
  // dict with signals storing values
36
- signals = parent ? Object.create(memo.get(parent = createState(parent))) : Array.isArray(values) ? [] : {},
37
- // proxy conducts prop access to signals
38
- state = new Proxy(signals, {
39
- // sandbox everything
40
- has() { return true },
41
- get(signals, key) {
42
- // console.log('get', key)
43
- let v
44
- // if .length is read within .push/etc - peek signal (don't subscribe)
45
- if (_len && key === 'length') v = Array.prototype[lastProp] ? _len.peek() : _len.value;
46
- else v = (signals[key] || initSignal(key))?.valueOf()
47
- if (_len) lastProp = key
48
- return v
49
- },
50
- set(signals, key, v) {
43
+ signals = parent ? Object.create((parent = createState(parent))[_signals]) : Array.isArray(values) ? [] : {},
44
+ proto = signals.constructor.prototype;
45
+
46
+ // proxy conducts prop access to signals
47
+ const state = new Proxy(values, {
48
+ // sandbox everything
49
+ has() { return true },
50
+ get(values, key) {
51
+ // if .length is read within .push/etc - peek signal (don't subscribe)
52
+ if (_len)
53
+ if (key === 'length') return (proto[lastProp]) ? _len.peek() : _len.value;
54
+ else lastProp = key;
55
+ if (proto[key]) return proto[key]
56
+ if (key === _signals) return signals
57
+ const s = signals[key] || initSignal(key)
58
+ if (s) return s.value // existing property
59
+ if (parent) return parent[key]; // touch parent
60
+ return sandbox[key] // Array, window etc
61
+ },
62
+ set(values, key, v) {
63
+ if (_len) {
51
64
  // .length
52
- if (_len && key === 'length') _len.value = signals.length = v;
53
-
54
- else {
55
- const s = signals[key] || initSignal(key)
56
-
57
- // new unknown property
58
- if (!s) signals[key] = signal(createState(v?.valueOf()))
59
- // stashed _set for values with getter/setter
60
- else if (s._set) s._set(v)
61
- // FIXME: is there meaningful way to update same-signature object?
62
- // else if (isObject(v) && isObject(s.value)) Object.assign(s.value, v)
63
- // .x = y
64
- else s.value = createState(v?.valueOf())
65
+ if (key === 'length') {
66
+ batch(() => {
67
+ // force cleaning up tail
68
+ for (let i = v, l = signals.length; i < l; i++) delete state[i]
69
+ _len.value = signals.length = values.length = v;
70
+ })
71
+ return true
65
72
  }
73
+ }
74
+
75
+ const s = signals[key] || initSignal(key, v) || signal()
76
+ const cur = s.peek()
66
77
 
67
- if (_len) lastProp = null
68
- return true
78
+ // skip unchanged (although can be handled by last condition - we skip a few checks this way)
79
+ if (v === cur);
80
+ // stashed _set for values with getter/setter
81
+ else if (s._set) s._set(v)
82
+ // patch array
83
+ else if (Array.isArray(v) && Array.isArray(cur)) {
84
+ untracked(() => batch(() => {
85
+ let i = 0, l = v.length, vals = values[key];
86
+ for (; i < l; i++) cur[i] = vals[i] = v[i]
87
+ cur.length = l // forces deleting tail signals
88
+ }))
89
+ }
90
+ // .x = y
91
+ else {
92
+ // reflect change in values
93
+ s.value = createState(values[key] = v)
69
94
  }
70
- })
95
+
96
+ // force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
97
+ if (_len && key >= _len.peek()) _len.value = signals.length = values.length = Number(key) + 1
98
+
99
+ return true
100
+ },
101
+ deleteProperty(values, key) {
102
+ signals[key]?._del?.(), delete signals[key], delete values[key]
103
+ return true
104
+ }
105
+ })
71
106
 
72
107
  // init signals placeholders (instead of ownKeys & getOwnPropertyDescriptor handlers)
73
- for (let key in values) {
74
- // FIXME: make lazy signals actually work (breaks 2 tests)
75
- // signals[key] = null // make placeholder
76
- signals[key] = initSignal(key)
77
- }
108
+ // if values are existing proxy (in case of extending parent) - take its signals instead of creating new ones
109
+ for (let key in values) signals[key] = initSignals?.[key] ?? initSignal(key);
78
110
 
79
111
  // initialize signal for provided key
80
112
  function initSignal(key) {
@@ -88,17 +120,10 @@ export default function createState(values, parent) {
88
120
  (signals[key] = computed(desc.get.bind(state)))._set = desc.set?.bind(state);
89
121
  return signals[key]
90
122
  }
123
+ // take over existing signal or create new signal
91
124
  return signals[key] = desc.value?.peek ? desc.value : signal(createState(desc.value))
92
125
  }
93
-
94
- // touch parent
95
- if (parent) return parent[key]
96
-
97
- // Array, window etc
98
- if (sandbox.hasOwnProperty(key)) return sandbox[key]
99
126
  }
100
127
 
101
- memo.set(state, signals)
102
-
103
128
  return state
104
129
  }
@@ -1,5 +1,5 @@
1
1
  // signals-based store implementation
2
- import { signal, computed, effect } from '@preact/signals-core'
2
+ import { signal, computed, effect, batch } from '@preact/signals-core'
3
3
  // import { signal, computed } from 'usignal/sync'
4
4
  // import { signal, computed } from '@webreflection/signal'
5
5
 
@@ -9,7 +9,7 @@ const isObject = v => v?.constructor === Object
9
9
 
10
10
  const _st = Symbol('state')
11
11
 
12
- export { effect as fx }
12
+ export { effect, batch }
13
13
 
14
14
  export default function createState(values, proto) {
15
15
  if (isState(values) && !proto) return values;