simplyview 3.0.6 → 3.1.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/dist/simply.app.js +25 -3
- package/dist/simply.app.min.js +1 -1
- package/dist/simply.app.min.js.map +3 -3
- package/dist/simply.everything.js +25 -4
- package/dist/simply.everything.min.js +1 -1
- package/dist/simply.everything.min.js.map +3 -3
- package/package.json +1 -1
- package/src/action.mjs +26 -3
- package/src/bind.mjs +0 -649
- package/src/model.mjs +0 -151
- package/src/state.mjs +0 -536
package/src/model.mjs
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import {signal, effect, batch} from './state.mjs'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This class implements a pluggable data model, where you can
|
|
5
|
-
* add effects that are run only when either an option for that
|
|
6
|
-
* effect changes, or when an effect earlier in the chain of
|
|
7
|
-
* effects changes.
|
|
8
|
-
*/
|
|
9
|
-
class SimplyModel {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new datamodel, with a state property that contains
|
|
13
|
-
* all the data passed to this constructor
|
|
14
|
-
* @param state Object with all the data for this model
|
|
15
|
-
*/
|
|
16
|
-
constructor(state) {
|
|
17
|
-
this.state = signal(state)
|
|
18
|
-
if (!this.state.options) {
|
|
19
|
-
this.state.options = {}
|
|
20
|
-
}
|
|
21
|
-
this.effects = [{current:state.data}]
|
|
22
|
-
this.view = signal(state.data)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Adds an effect to run whenever a signal it depends on
|
|
27
|
-
* changes. this.state is the usual signal.
|
|
28
|
-
* The `fn` function param is not itself an effect, but must return
|
|
29
|
-
* and effect function. `fn` takes one param, which is the data signal.
|
|
30
|
-
* This signal will always have at least a `current` property.
|
|
31
|
-
* The result of the effect function is pushed on to the this.effects
|
|
32
|
-
* list. And the last effect added is set as this.view
|
|
33
|
-
*/
|
|
34
|
-
addEffect(fn) {
|
|
35
|
-
const dataSignal = this.effects[this.effects.length-1]
|
|
36
|
-
this.view = fn.call(this, dataSignal)
|
|
37
|
-
this.effects.push(this.view)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function model(options) {
|
|
42
|
-
return new SimplyModel(options)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function sort(options={}) {
|
|
46
|
-
return function(data) {
|
|
47
|
-
// initialize the sort options, only gets called once
|
|
48
|
-
this.state.options.sort = Object.assign({
|
|
49
|
-
direction: 'asc',
|
|
50
|
-
sortBy: null,
|
|
51
|
-
sortFn: ((a,b) => {
|
|
52
|
-
const sort = this.state.options.sort
|
|
53
|
-
const sortBy = sort.sortBy
|
|
54
|
-
if (!sort.sortBy) {
|
|
55
|
-
return 0
|
|
56
|
-
}
|
|
57
|
-
const larger = sort.direction == 'asc' ? 1 : -1
|
|
58
|
-
const smaller = sort.direction == 'asc' ? -1 : 1
|
|
59
|
-
if (typeof a?.[sortBy] === 'undefined') {
|
|
60
|
-
if (typeof b?.[sortBy] === 'undefined') {
|
|
61
|
-
return 0
|
|
62
|
-
}
|
|
63
|
-
return larger
|
|
64
|
-
}
|
|
65
|
-
if (typeof b?.[sortBy] === 'undefined') {
|
|
66
|
-
return smaller
|
|
67
|
-
}
|
|
68
|
-
if (a[sortBy]<b[sortBy]) {
|
|
69
|
-
return smaller
|
|
70
|
-
} else if (a[sortBy]>b[sortBy]) {
|
|
71
|
-
return larger
|
|
72
|
-
} else {
|
|
73
|
-
return 0
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
}, options);
|
|
77
|
-
// then return the effect, which is called when
|
|
78
|
-
// either the data or the sort options change
|
|
79
|
-
return effect(() => {
|
|
80
|
-
const sort = this.state.options.sort
|
|
81
|
-
if (sort?.sortBy && sort?.direction) {
|
|
82
|
-
return data.current.toSorted(sort?.sortFn)
|
|
83
|
-
}
|
|
84
|
-
return data.current
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function paging(options={}) {
|
|
90
|
-
return function(data) {
|
|
91
|
-
// initialize the paging options
|
|
92
|
-
this.state.options.paging = Object.assign({
|
|
93
|
-
page: 1,
|
|
94
|
-
pageSize: 20,
|
|
95
|
-
max: 1
|
|
96
|
-
}, options)
|
|
97
|
-
return effect(() => {
|
|
98
|
-
return batch(() => {
|
|
99
|
-
const paging = this.state.options.paging
|
|
100
|
-
if (!paging.pageSize) {
|
|
101
|
-
paging.pageSize = 20
|
|
102
|
-
}
|
|
103
|
-
paging.max = Math.ceil(this.state.data.length / paging.pageSize)
|
|
104
|
-
paging.page = Math.max(1, Math.min(paging.max, paging.page))
|
|
105
|
-
|
|
106
|
-
const start = (paging.page-1) * paging.pageSize
|
|
107
|
-
const end = start + paging.pageSize
|
|
108
|
-
return data.current.slice(start, end)
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function filter(options) {
|
|
115
|
-
if (!options?.name || typeof options.name!=='string') {
|
|
116
|
-
throw new Error('filter requires options.name to be a string')
|
|
117
|
-
}
|
|
118
|
-
if (!options.matches || typeof options.matches!=='function') {
|
|
119
|
-
throw new Error('filter requires options.matches to be a function')
|
|
120
|
-
}
|
|
121
|
-
return function(data) {
|
|
122
|
-
this.state.options[options.name] = options
|
|
123
|
-
return effect(() => {
|
|
124
|
-
if (this.state.options[options.name].enabled) {
|
|
125
|
-
return data.filter(this.state.options.matches)
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function columns(options={}) {
|
|
132
|
-
if (!options
|
|
133
|
-
|| typeof options!=='object'
|
|
134
|
-
|| Object.keys(options).length===0) {
|
|
135
|
-
throw new Error('columns requires options to be an object with at least one property')
|
|
136
|
-
}
|
|
137
|
-
return function(data) {
|
|
138
|
-
this.state.options.columns = options
|
|
139
|
-
return effect(() => {
|
|
140
|
-
return data.current.map(input => {
|
|
141
|
-
let result = {}
|
|
142
|
-
for (let key of Object.keys(this.state.options.columns)) {
|
|
143
|
-
if (!this.state.options.columns[key].hidden) {
|
|
144
|
-
result[key] = input[key]
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return result
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
}
|
package/src/state.mjs
DELETED
|
@@ -1,536 +0,0 @@
|
|
|
1
|
-
const iterate = Symbol('iterate')
|
|
2
|
-
if (!Symbol.xRay) {
|
|
3
|
-
Symbol.xRay = Symbol('xRay')
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const signalHandler = {
|
|
7
|
-
get: (target, property, receiver) => {
|
|
8
|
-
if (property===Symbol.xRay) {
|
|
9
|
-
return target // don't notifyGet here, this is only called by set
|
|
10
|
-
}
|
|
11
|
-
const value = target?.[property] // Reflect.get fails on a Set.
|
|
12
|
-
notifyGet(receiver, property)
|
|
13
|
-
if (typeof value === 'function') {
|
|
14
|
-
if (Array.isArray(target)) {
|
|
15
|
-
return (...args) => {
|
|
16
|
-
let l = target.length
|
|
17
|
-
// by binding the function to the receiver
|
|
18
|
-
// all accesses in the function will be trapped
|
|
19
|
-
// by the Proxy, so get/set/delete is all handled
|
|
20
|
-
let result = value.apply(receiver, args)
|
|
21
|
-
if (l != target.length) {
|
|
22
|
-
notifySet(receiver, makeContext('length', { was: l, now: target.length }) )
|
|
23
|
-
}
|
|
24
|
-
return result
|
|
25
|
-
}
|
|
26
|
-
} else if (target instanceof Set || target instanceof Map) {
|
|
27
|
-
return (...args) => {
|
|
28
|
-
// node doesn't allow you to call set/map functions
|
|
29
|
-
// bound to the receiver.. so using target instead
|
|
30
|
-
// there are no properties to update anyway, except for size
|
|
31
|
-
let s = target.size
|
|
32
|
-
let result = value.apply(target, args)
|
|
33
|
-
if (s != target.size) {
|
|
34
|
-
notifySet(receiver, makeContext( 'size', { was: s, now: target.size }) )
|
|
35
|
-
}
|
|
36
|
-
// there is no efficient way to see if the function called
|
|
37
|
-
// has actually changed the Set/Map, but by assuming the
|
|
38
|
-
// 'setter' functions will change the results of the
|
|
39
|
-
// 'getter' functions, effects should update correctly
|
|
40
|
-
if (['set','add','clear','delete'].includes(property)) {
|
|
41
|
-
notifySet(receiver, makeContext( { entries: {}, forEach: {}, has: {}, keys: {}, values: {}, [Symbol.iterator]: {} } ) )
|
|
42
|
-
}
|
|
43
|
-
return result
|
|
44
|
-
}
|
|
45
|
-
} else if (
|
|
46
|
-
target instanceof HTMLElement
|
|
47
|
-
|| target instanceof Number
|
|
48
|
-
|| target instanceof String
|
|
49
|
-
|| target instanceof Boolean
|
|
50
|
-
) {
|
|
51
|
-
return value.bind(target)
|
|
52
|
-
} else {
|
|
53
|
-
// support custom classes, hopefully
|
|
54
|
-
return value.bind(receiver)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (value && typeof value == 'object') {
|
|
58
|
-
//NOTE: get now returns a signal, set doesn't 'unsignal' the value set
|
|
59
|
-
return signal(value)
|
|
60
|
-
}
|
|
61
|
-
return value
|
|
62
|
-
},
|
|
63
|
-
set: (target, property, value, receiver) => {
|
|
64
|
-
value = value?.[Symbol.xRay] || value // unwraps signal
|
|
65
|
-
let current = target[property]
|
|
66
|
-
if (current!==value) {
|
|
67
|
-
target[property] = value
|
|
68
|
-
notifySet(receiver, makeContext(property, { was: current, now: value } ) )
|
|
69
|
-
}
|
|
70
|
-
if (typeof current === 'undefined') {
|
|
71
|
-
notifySet(receiver, makeContext(iterate, {}))
|
|
72
|
-
}
|
|
73
|
-
return true
|
|
74
|
-
},
|
|
75
|
-
has: (target, property) => { // receiver is not part of the has() call
|
|
76
|
-
let receiver = signals.get(target) // so retrieve it here
|
|
77
|
-
if (receiver) {
|
|
78
|
-
notifyGet(receiver, property)
|
|
79
|
-
}
|
|
80
|
-
return Object.hasOwn(target, property)
|
|
81
|
-
},
|
|
82
|
-
deleteProperty: (target, property) => {
|
|
83
|
-
if (typeof target[property] !== 'undefined') {
|
|
84
|
-
let current = target[property]
|
|
85
|
-
delete target[property]
|
|
86
|
-
let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
|
|
87
|
-
notifySet(receiver, makeContext(property,{ delete: true, was: current }))
|
|
88
|
-
}
|
|
89
|
-
return true
|
|
90
|
-
},
|
|
91
|
-
defineProperty: (target, property, descriptor) => {
|
|
92
|
-
if (typeof target[property] === 'undefined') {
|
|
93
|
-
let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
|
|
94
|
-
notifySet(receiver, makeContext(iterate, {}))
|
|
95
|
-
}
|
|
96
|
-
return Object.defineProperty(target, property, descriptor)
|
|
97
|
-
},
|
|
98
|
-
ownKeys: (target) => {
|
|
99
|
-
let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
|
|
100
|
-
notifyGet(receiver, iterate)
|
|
101
|
-
return Reflect.ownKeys(target)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Keeps track of the return signal for an update function, as well
|
|
108
|
-
* as signals connected to other objects.
|
|
109
|
-
* Makes sure that a given object or function always uses the same
|
|
110
|
-
* signal
|
|
111
|
-
*/
|
|
112
|
-
const signals = new WeakMap()
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Creates a new signal proxy of the given object, that intercepts get/has and set/delete
|
|
116
|
-
* to allow reactive functions to be triggered when signal values change.
|
|
117
|
-
*/
|
|
118
|
-
export function signal(v) {
|
|
119
|
-
if (!signals.has(v)) {
|
|
120
|
-
signals.set(v, new Proxy(v, signalHandler))
|
|
121
|
-
}
|
|
122
|
-
return signals.get(v)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let batchedListeners = new Set()
|
|
126
|
-
let batchMode = 0
|
|
127
|
-
/**
|
|
128
|
-
* Called when a signal changes a property (set/delete)
|
|
129
|
-
* Triggers any reactor function that depends on this signal
|
|
130
|
-
* to re-compute its values
|
|
131
|
-
*/
|
|
132
|
-
function notifySet(self, context={}) {
|
|
133
|
-
let listeners = []
|
|
134
|
-
context.forEach((change, property) => {
|
|
135
|
-
let propListeners = getListeners(self, property)
|
|
136
|
-
if (propListeners?.length) {
|
|
137
|
-
for (let listener of propListeners) {
|
|
138
|
-
addContext(listener, makeContext(property,change))
|
|
139
|
-
}
|
|
140
|
-
listeners = listeners.concat(propListeners)
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
listeners = new Set(listeners.filter(Boolean))
|
|
144
|
-
if (listeners) {
|
|
145
|
-
if (batchMode) {
|
|
146
|
-
batchedListeners = batchedListeners.union(listeners)
|
|
147
|
-
} else {
|
|
148
|
-
const currentEffect = computeStack[computeStack.length-1]
|
|
149
|
-
for (let listener of Array.from(listeners)) {
|
|
150
|
-
if (listener!=currentEffect && listener?.needsUpdate) {
|
|
151
|
-
listener()
|
|
152
|
-
}
|
|
153
|
-
clearContext(listener)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function makeContext(property, change) {
|
|
160
|
-
let context = new Map()
|
|
161
|
-
if (typeof property === 'object') {
|
|
162
|
-
for (let prop in property) {
|
|
163
|
-
context.set(prop, property[prop])
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
context.set(property, change)
|
|
167
|
-
}
|
|
168
|
-
return context
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function addContext(listener, context) {
|
|
172
|
-
if (!listener.context) {
|
|
173
|
-
listener.context = context
|
|
174
|
-
} else {
|
|
175
|
-
context.forEach((change,property)=> {
|
|
176
|
-
listener.context.set(property, change) // TODO: merge change if needed
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
listener.needsUpdate = true
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function clearContext(listener) {
|
|
183
|
-
delete listener.context
|
|
184
|
-
delete listener.needsUpdate
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Called when a signal property is accessed. If this happens
|
|
189
|
-
* inside a reactor function--computeStack is not empty--
|
|
190
|
-
* then it adds the current reactor (top of this stack) to its
|
|
191
|
-
* listeners. These are later called if this property changes
|
|
192
|
-
*/
|
|
193
|
-
function notifyGet(self, property) {
|
|
194
|
-
let currentCompute = computeStack[computeStack.length-1]
|
|
195
|
-
if (currentCompute) {
|
|
196
|
-
// get was part of a react() function, so add it
|
|
197
|
-
setListeners(self, property, currentCompute)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Keeps track of which update() functions are dependent on which
|
|
203
|
-
* signal objects and which properties. Maps signals to update fns
|
|
204
|
-
*/
|
|
205
|
-
const listenersMap = new WeakMap()
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Keeps track of which signals and properties are linked to which
|
|
209
|
-
* update functions. Maps update functions and properties to signals
|
|
210
|
-
*/
|
|
211
|
-
const computeMap = new WeakMap()
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Returns the update functions for a given signal and property
|
|
215
|
-
*/
|
|
216
|
-
function getListeners(self, property) {
|
|
217
|
-
let listeners = listenersMap.get(self)
|
|
218
|
-
return listeners ? Array.from(listeners.get(property) || []) : []
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Adds an update function (compute) to the list of listeners on
|
|
223
|
-
* the given signal (self) and property
|
|
224
|
-
*/
|
|
225
|
-
function setListeners(self, property, compute) {
|
|
226
|
-
if (!listenersMap.has(self)) {
|
|
227
|
-
listenersMap.set(self, new Map())
|
|
228
|
-
}
|
|
229
|
-
let listeners = listenersMap.get(self)
|
|
230
|
-
if (!listeners.has(property)) {
|
|
231
|
-
listeners.set(property, new Set())
|
|
232
|
-
}
|
|
233
|
-
listeners.get(property).add(compute)
|
|
234
|
-
|
|
235
|
-
if (!computeMap.has(compute)) {
|
|
236
|
-
computeMap.set(compute, new Map())
|
|
237
|
-
}
|
|
238
|
-
let connectedSignals = computeMap.get(compute)
|
|
239
|
-
if (!connectedSignals.has(property)) {
|
|
240
|
-
connectedSignals.set(property, new Set)
|
|
241
|
-
}
|
|
242
|
-
connectedSignals.get(property).add(self)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Removes alle listeners that trigger the given reactor function (compute)
|
|
247
|
-
* This happens when a reactor is called, so that it can set new listeners
|
|
248
|
-
* based on the current call (code path)
|
|
249
|
-
*/
|
|
250
|
-
function clearListeners(compute) {
|
|
251
|
-
let connectedSignals = computeMap.get(compute)
|
|
252
|
-
if (connectedSignals) {
|
|
253
|
-
connectedSignals.forEach(property => {
|
|
254
|
-
property.forEach(s => {
|
|
255
|
-
let listeners = listenersMap.get(s)
|
|
256
|
-
if (listeners.has(property)) {
|
|
257
|
-
listeners.get(property).delete(compute)
|
|
258
|
-
}
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* The top most entry is the currently running update function, used
|
|
266
|
-
* to automatically record signals used in an update function.
|
|
267
|
-
*/
|
|
268
|
-
let computeStack = []
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Used for cycle detection: effectStack contains all running effect
|
|
272
|
-
* functions. If the same function appears twice in this stack, there
|
|
273
|
-
* is a recursive update call, which would cause an infinite loop.
|
|
274
|
-
*/
|
|
275
|
-
const effectStack = []
|
|
276
|
-
|
|
277
|
-
const effectMap = new WeakMap()
|
|
278
|
-
/**
|
|
279
|
-
* Used for cycle detection: signalStack contains all used signals.
|
|
280
|
-
* If the same signal appears more than once, there is a cyclical
|
|
281
|
-
* dependency between signals, which would cause an infinite loop.
|
|
282
|
-
*/
|
|
283
|
-
const signalStack = []
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Runs the given function at once, and then whenever a signal changes that
|
|
287
|
-
* is used by the given function (or at least signals used in the previous run).
|
|
288
|
-
*/
|
|
289
|
-
export function effect(fn) {
|
|
290
|
-
if (effectStack.findIndex(f => fn==f)!==-1) {
|
|
291
|
-
throw new Error('Recursive update() call', {cause:fn})
|
|
292
|
-
}
|
|
293
|
-
effectStack.push(fn)
|
|
294
|
-
|
|
295
|
-
let connectedSignal = signals.get(fn)
|
|
296
|
-
if (!connectedSignal) {
|
|
297
|
-
connectedSignal = signal({
|
|
298
|
-
current: null
|
|
299
|
-
})
|
|
300
|
-
signals.set(fn, connectedSignal)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// this is the function that is called automatically
|
|
304
|
-
// whenever a signal dependency changes
|
|
305
|
-
const computeEffect = function computeEffect() {
|
|
306
|
-
if (signalStack.findIndex(s => s==connectedSignal)!==-1) {
|
|
307
|
-
throw new Error('Cyclical dependency in update() call', { cause: fn})
|
|
308
|
-
}
|
|
309
|
-
// remove all dependencies (signals) from previous runs
|
|
310
|
-
clearListeners(computeEffect)
|
|
311
|
-
// record new dependencies on this run
|
|
312
|
-
computeStack.push(computeEffect)
|
|
313
|
-
// prevent recursion
|
|
314
|
-
signalStack.push(connectedSignal)
|
|
315
|
-
// call the actual update function
|
|
316
|
-
let result
|
|
317
|
-
try {
|
|
318
|
-
result = fn(computeEffect, computeStack, signalStack)
|
|
319
|
-
} finally {
|
|
320
|
-
// stop recording dependencies
|
|
321
|
-
computeStack.pop()
|
|
322
|
-
// stop the recursion prevention
|
|
323
|
-
signalStack.pop()
|
|
324
|
-
if (result instanceof Promise) {
|
|
325
|
-
result.then((result) => {
|
|
326
|
-
connectedSignal.current = result
|
|
327
|
-
})
|
|
328
|
-
} else {
|
|
329
|
-
connectedSignal.current = result
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
computeEffect.fn = fn
|
|
334
|
-
effectMap.set(connectedSignal, computeEffect)
|
|
335
|
-
|
|
336
|
-
// run the computEffect immediately upon creation
|
|
337
|
-
computeEffect()
|
|
338
|
-
return connectedSignal
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
export function destroy(connectedSignal) {
|
|
343
|
-
// find the computeEffect associated with this signal
|
|
344
|
-
const computeEffect = effectMap.get(connectedSignal)?.deref()
|
|
345
|
-
if (!computeEffect) {
|
|
346
|
-
return
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// remove all listeners for this effect
|
|
350
|
-
clearListeners(computeEffect)
|
|
351
|
-
|
|
352
|
-
// remove all references to connectedSignal
|
|
353
|
-
let fn = computeEffect.fn
|
|
354
|
-
signals.remove(fn)
|
|
355
|
-
|
|
356
|
-
effectMap.delete(connectedSignal)
|
|
357
|
-
|
|
358
|
-
// if no other references to connectedSignal exist, it will be garbage collected
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Inside a batch() call, any changes to signals do not trigger effects
|
|
363
|
-
* immediately. Instead, immediately after finishing the batch() call,
|
|
364
|
-
* these effects will be called. Effects that are triggered by multiple
|
|
365
|
-
* signals are called only once.
|
|
366
|
-
* @param Function fn batch() calls this function immediately
|
|
367
|
-
* @result mixed the result of the fn() function call
|
|
368
|
-
*/
|
|
369
|
-
export function batch(fn) {
|
|
370
|
-
batchMode++
|
|
371
|
-
let result
|
|
372
|
-
try {
|
|
373
|
-
result = fn()
|
|
374
|
-
} finally {
|
|
375
|
-
if (result instanceof Promise) {
|
|
376
|
-
result.then(() => {
|
|
377
|
-
batchMode--
|
|
378
|
-
if (!batchMode) {
|
|
379
|
-
runBatchedListeners()
|
|
380
|
-
}
|
|
381
|
-
})
|
|
382
|
-
} else {
|
|
383
|
-
batchMode--
|
|
384
|
-
if (!batchMode) {
|
|
385
|
-
runBatchedListeners()
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return result
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function runBatchedListeners() {
|
|
393
|
-
let copyBatchedListeners = Array.from(batchedListeners)
|
|
394
|
-
batchedListeners = new Set()
|
|
395
|
-
const currentEffect = computeStack[computeStack.length-1]
|
|
396
|
-
for (let listener of copyBatchedListeners) {
|
|
397
|
-
if (listener!=currentEffect && listener?.needsUpdate) {
|
|
398
|
-
listener()
|
|
399
|
-
}
|
|
400
|
-
clearContext(listener)
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* A throttledEffect is run immediately once. And then only once
|
|
406
|
-
* per throttleTime (in ms).
|
|
407
|
-
* @param Function fn the effect function to run whenever a signal changes
|
|
408
|
-
* @param int throttleTime in ms
|
|
409
|
-
* @returns signal with the result of the effect function fn
|
|
410
|
-
*/
|
|
411
|
-
export function throttledEffect(fn, throttleTime) {
|
|
412
|
-
if (effectStack.findIndex(f => fn==f)!==-1) {
|
|
413
|
-
throw new Error('Recursive update() call', {cause:fn})
|
|
414
|
-
}
|
|
415
|
-
effectStack.push(fn)
|
|
416
|
-
|
|
417
|
-
let connectedSignal = signals.get(fn)
|
|
418
|
-
if (!connectedSignal) {
|
|
419
|
-
connectedSignal = signal({
|
|
420
|
-
current: null
|
|
421
|
-
})
|
|
422
|
-
signals.set(fn, connectedSignal)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
let throttled = false
|
|
426
|
-
let hasChange = true
|
|
427
|
-
// this is the function that is called automatically
|
|
428
|
-
// whenever a signal dependency changes
|
|
429
|
-
const computeEffect = function computeEffect() {
|
|
430
|
-
if (signalStack.findIndex(s => s==connectedSignal)!==-1) {
|
|
431
|
-
throw new Error('Cyclical dependency in update() call', { cause: fn})
|
|
432
|
-
}
|
|
433
|
-
if (throttled && throttled>Date.now()) {
|
|
434
|
-
hasChange = true
|
|
435
|
-
return
|
|
436
|
-
}
|
|
437
|
-
// remove all dependencies (signals) from previous runs
|
|
438
|
-
clearListeners(computeEffect)
|
|
439
|
-
// record new dependencies on this run
|
|
440
|
-
computeStack.push(computeEffect)
|
|
441
|
-
// prevent recursion
|
|
442
|
-
signalStack.push(connectedSignal)
|
|
443
|
-
// call the actual update function
|
|
444
|
-
let result
|
|
445
|
-
try {
|
|
446
|
-
result = fn(computeEffect, computeStack, signalStack)
|
|
447
|
-
} finally {
|
|
448
|
-
hasChange = false
|
|
449
|
-
// stop recording dependencies
|
|
450
|
-
computeStack.pop()
|
|
451
|
-
// stop the recursion prevention
|
|
452
|
-
signalStack.pop()
|
|
453
|
-
if (result instanceof Promise) {
|
|
454
|
-
result.then((result) => {
|
|
455
|
-
connectedSignal.current = result
|
|
456
|
-
})
|
|
457
|
-
} else {
|
|
458
|
-
connectedSignal.current = result
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
throttled = Date.now()+throttleTime
|
|
462
|
-
globalThis.setTimeout(() => {
|
|
463
|
-
if (hasChange) {
|
|
464
|
-
computeEffect()
|
|
465
|
-
}
|
|
466
|
-
}, throttleTime)
|
|
467
|
-
}
|
|
468
|
-
// run the computEffect immediately upon creation
|
|
469
|
-
computeEffect()
|
|
470
|
-
return connectedSignal
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// refactor: Class clock() with an effect() method
|
|
474
|
-
// keep track of effects per clock, and add clock property to the effect function
|
|
475
|
-
// on notifySet add clock.effects to clock.needsUpdate list
|
|
476
|
-
// on clock.tick() (or clock.time++) run only the clock.needsUpdate effects
|
|
477
|
-
// (first create a copy and reset clock.needsUpdate, then run effects)
|
|
478
|
-
export function clockEffect(fn, clock) {
|
|
479
|
-
let connectedSignal = signals.get(fn)
|
|
480
|
-
if (!connectedSignal) {
|
|
481
|
-
connectedSignal = signal({
|
|
482
|
-
current: null
|
|
483
|
-
})
|
|
484
|
-
signals.set(fn, connectedSignal)
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
let lastTick = -1 // clock.time should start at 0 or larger
|
|
488
|
-
let hasChanged = true // make sure the first run goes through
|
|
489
|
-
// this is the function that is called automatically
|
|
490
|
-
// whenever a signal dependency changes
|
|
491
|
-
const computeEffect = function computeEffect() {
|
|
492
|
-
if (lastTick < clock.time) {
|
|
493
|
-
if (hasChanged) {
|
|
494
|
-
// remove all dependencies (signals) from previous runs
|
|
495
|
-
clearListeners(computeEffect)
|
|
496
|
-
// record new dependencies on this run
|
|
497
|
-
computeStack.push(computeEffect)
|
|
498
|
-
// make sure the clock.time signal is a dependency
|
|
499
|
-
lastTick = clock.time
|
|
500
|
-
// call the actual update function
|
|
501
|
-
let result
|
|
502
|
-
try {
|
|
503
|
-
result = fn(computeEffect, computeStack)
|
|
504
|
-
} finally {
|
|
505
|
-
// stop recording dependencies
|
|
506
|
-
computeStack.pop()
|
|
507
|
-
if (result instanceof Promise) {
|
|
508
|
-
result.then((result) => {
|
|
509
|
-
connectedSignal.current = result
|
|
510
|
-
})
|
|
511
|
-
} else {
|
|
512
|
-
connectedSignal.current = result
|
|
513
|
-
}
|
|
514
|
-
hasChanged = false
|
|
515
|
-
}
|
|
516
|
-
} else {
|
|
517
|
-
lastTick = clock.time
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
hasChanged = true
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// run the computEffect immediately upon creation
|
|
524
|
-
computeEffect()
|
|
525
|
-
return connectedSignal
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
export function untracked(fn) {
|
|
529
|
-
const remember = computeStack.slice()
|
|
530
|
-
computeStack = []
|
|
531
|
-
try {
|
|
532
|
-
return fn()
|
|
533
|
-
} finally {
|
|
534
|
-
computeStack = remember
|
|
535
|
-
}
|
|
536
|
-
}
|