sprae 6.1.2 → 8.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/package.json +3 -5
- package/readme.md +75 -13
- package/sprae.auto.js +682 -327
- package/sprae.auto.min.js +1 -1
- package/sprae.js +682 -327
- package/sprae.min.js +1 -1
- package/src/core.js +24 -25
- package/src/directives.js +109 -83
- package/src/domdiff.js +10 -7
- package/src/{state.js → state.proxy.js} +40 -17
- package/src/state.signals-proxy.js +133 -0
- package/src/state.signals.js +82 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// signals-based proxy
|
|
2
|
+
// + we need proxy for sandbox & arrays anyways
|
|
3
|
+
// + it seems faster than defining a bunch of props on sealed state object
|
|
4
|
+
// + we need support signal inputs
|
|
5
|
+
// + signals provide nice tracking mechanism, unlike own arrays
|
|
6
|
+
// + signals detect cycles
|
|
7
|
+
// + it's just robust
|
|
8
|
+
// ? must it modify initial store
|
|
9
|
+
|
|
10
|
+
import { signal, computed, effect, batch, untracked } from '@preact/signals-core'
|
|
11
|
+
// import { signal, computed } from 'usignal/sync'
|
|
12
|
+
// import { signal, computed } from '@webreflection/signal'
|
|
13
|
+
|
|
14
|
+
export { effect, computed, batch, untracked }
|
|
15
|
+
|
|
16
|
+
export const _dispose = (Symbol.dispose ||= Symbol('dispose'));
|
|
17
|
+
export const _signals = Symbol('signals');
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// default root sandbox
|
|
21
|
+
export const sandbox = {
|
|
22
|
+
Array, Object, Number, String, Boolean, Date,
|
|
23
|
+
console, window, document, history, navigator, location, screen, localStorage, sessionStorage,
|
|
24
|
+
alert, prompt, confirm, fetch, performance,
|
|
25
|
+
setTimeout, setInterval, requestAnimationFrame
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isObject = v => v?.constructor === Object
|
|
29
|
+
const isPrimitive = (value) => value !== Object(value);
|
|
30
|
+
|
|
31
|
+
// track last accessed property to figure out if .length was directly accessed from expression or via .push/etc method
|
|
32
|
+
let lastProp
|
|
33
|
+
|
|
34
|
+
export default function createState(values, parent) {
|
|
35
|
+
if (!isObject(values) && !Array.isArray(values)) return values;
|
|
36
|
+
// ignore existing state as argument
|
|
37
|
+
if (values[_signals] && !parent) return values;
|
|
38
|
+
const initSignals = values[_signals]
|
|
39
|
+
|
|
40
|
+
// .length signal is stored outside, since cannot be replaced
|
|
41
|
+
const _len = Array.isArray(values) && signal((initSignals || values).length),
|
|
42
|
+
// dict with signals storing values
|
|
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
|
+
if (sandbox.hasOwnProperty(key)) return sandbox[key] // Array, window etc
|
|
61
|
+
},
|
|
62
|
+
set(values, key, v) {
|
|
63
|
+
if (_len) {
|
|
64
|
+
// .length
|
|
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
|
+
// force init new signals
|
|
70
|
+
for (let i = signals.length; i < v; i++) state[i] = null
|
|
71
|
+
_len.value = signals.length = values.length = v;
|
|
72
|
+
})
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const s = signals[key] || initSignal(key, v) || signal()
|
|
78
|
+
const cur = s.peek()
|
|
79
|
+
|
|
80
|
+
// skip unchanged (although can be handled by last condition - we skip a few checks this way)
|
|
81
|
+
if (v === cur);
|
|
82
|
+
// stashed _set for values with getter/setter
|
|
83
|
+
else if (s._set) s._set(v)
|
|
84
|
+
// patch array
|
|
85
|
+
else if (Array.isArray(v) && Array.isArray(cur)) {
|
|
86
|
+
untracked(() => batch(() => {
|
|
87
|
+
let i = 0, l = v.length;
|
|
88
|
+
for (; i < l; i++) cur[i] = values[key][i] = v[i]
|
|
89
|
+
cur.length = l // forces deleting tail signals
|
|
90
|
+
s.value = null, s.value = cur // bump effects
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
// .x = y
|
|
94
|
+
else {
|
|
95
|
+
// reflect change in values
|
|
96
|
+
if (Array.isArray(cur)) cur.length = 0 // cleanup array subs
|
|
97
|
+
s.value = createState(values[key] = v)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
|
|
101
|
+
if (_len && key >= _len.peek()) _len.value = signals.length = values.length = Number(key) + 1
|
|
102
|
+
|
|
103
|
+
return true
|
|
104
|
+
},
|
|
105
|
+
deleteProperty(values, key) {
|
|
106
|
+
if (key in signals) signals[key]._del?.(), delete signals[key], delete values[key]
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// init signals placeholders (instead of ownKeys & getOwnPropertyDescriptor handlers)
|
|
112
|
+
// if values are existing proxy (in case of extending parent) - take its signals instead of creating new ones
|
|
113
|
+
for (let key in values) values[key], signals[key] = initSignals?.[key] ?? null;
|
|
114
|
+
|
|
115
|
+
// initialize signal for provided key
|
|
116
|
+
function initSignal(key) {
|
|
117
|
+
// init existing value
|
|
118
|
+
if (values.hasOwnProperty(key)) {
|
|
119
|
+
// create signal from descriptor
|
|
120
|
+
const desc = Object.getOwnPropertyDescriptor(values, key)
|
|
121
|
+
// getter turns into computed
|
|
122
|
+
if (desc?.get) {
|
|
123
|
+
// stash setter
|
|
124
|
+
(signals[key] = computed(desc.get.bind(state)))._set = desc.set?.bind(state);
|
|
125
|
+
return signals[key]
|
|
126
|
+
}
|
|
127
|
+
// take over existing signal or create new signal
|
|
128
|
+
return signals[key] = desc.value?.peek ? desc.value : signal(createState(desc.value))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return state
|
|
133
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// signals-based store implementation
|
|
2
|
+
import { signal, computed, effect, batch } from '@preact/signals-core'
|
|
3
|
+
// import { signal, computed } from 'usignal/sync'
|
|
4
|
+
// import { signal, computed } from '@webreflection/signal'
|
|
5
|
+
|
|
6
|
+
const isSignal = v => v?.peek
|
|
7
|
+
const isState = v => v?.[_st]
|
|
8
|
+
const isObject = v => v?.constructor === Object
|
|
9
|
+
|
|
10
|
+
const _st = Symbol('state')
|
|
11
|
+
|
|
12
|
+
export { effect, batch }
|
|
13
|
+
|
|
14
|
+
export default function createState(values, proto) {
|
|
15
|
+
if (isState(values) && !proto) return values;
|
|
16
|
+
|
|
17
|
+
// define signal accessors - creates signals for all object props
|
|
18
|
+
|
|
19
|
+
if (isObject(values)) {
|
|
20
|
+
const
|
|
21
|
+
state = Object.create(proto || Object.getPrototypeOf(values)),
|
|
22
|
+
signals = {},
|
|
23
|
+
descs = Object.getOwnPropertyDescriptors(values)
|
|
24
|
+
|
|
25
|
+
// define signal accessors for exported object
|
|
26
|
+
for (let key in descs) {
|
|
27
|
+
let desc = descs[key]
|
|
28
|
+
|
|
29
|
+
// getter turns into computed
|
|
30
|
+
if (desc.get) {
|
|
31
|
+
let s = signals[key] = computed(desc.get.bind(state))
|
|
32
|
+
Object.defineProperty(state, key, {
|
|
33
|
+
get() { return s.value },
|
|
34
|
+
set: desc.set?.bind(state),
|
|
35
|
+
configurable: false,
|
|
36
|
+
enumerable: true
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
// regular value creates signal accessor
|
|
40
|
+
else {
|
|
41
|
+
let value = desc.value
|
|
42
|
+
let s = signals[key] = isSignal(value) ? value :
|
|
43
|
+
signal(
|
|
44
|
+
// if initial value is an object - we turn it into sealed struct
|
|
45
|
+
isObject(value) ? Object.seal(createState(value)) :
|
|
46
|
+
Array.isArray(value) ? createState(value) :
|
|
47
|
+
value
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// define property accessor on struct
|
|
51
|
+
Object.defineProperty(state, key, {
|
|
52
|
+
get() { return s.value },
|
|
53
|
+
set(v) {
|
|
54
|
+
if (isObject(v)) {
|
|
55
|
+
// new object can have another schema than the new one
|
|
56
|
+
// so if it throws due to new props access then we fall back to creating new struct
|
|
57
|
+
if (isObject(s.value)) try { Object.assign(s.value, v); return } catch (e) { }
|
|
58
|
+
s.value = Object.seal(createState(v));
|
|
59
|
+
}
|
|
60
|
+
else if (Array.isArray(v)) s.value = createState(v)
|
|
61
|
+
else s.value = v;
|
|
62
|
+
|
|
63
|
+
},
|
|
64
|
+
enumerable: true,
|
|
65
|
+
configurable: false
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Object.defineProperty(state, _st, { configurable: false, enumerable: false, value: true })
|
|
71
|
+
|
|
72
|
+
return state
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// for arrays we turn internals into signal structs
|
|
76
|
+
// FIXME: make proxy to intercept length and other single values
|
|
77
|
+
if (Array.isArray(values)) {
|
|
78
|
+
for (let i = 0; i < values.length; i++) values[i] = createState(values[i])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return values
|
|
82
|
+
}
|