@muze-labs/simplyflow 0.9.0 → 0.10.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/README.md +72 -16
- package/dist/simply.flow.js +458 -298
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +4 -4
- package/package.json +29 -42
- package/src/action.mjs +1 -64
- package/src/app.mjs +1 -282
- package/src/behavior.mjs +1 -121
- package/src/bind-render.mjs +1 -0
- package/src/bind-transformers.mjs +1 -0
- package/src/bind.mjs +1 -522
- package/src/command.mjs +1 -225
- package/src/dom.mjs +1 -274
- package/src/highlight.mjs +1 -11
- package/src/include.mjs +1 -239
- package/src/{flow.mjs → index.mjs} +13 -13
- package/src/model.mjs +1 -290
- package/src/path.mjs +1 -47
- package/src/route.mjs +1 -418
- package/src/shortcut.mjs +1 -146
- package/src/state.mjs +1 -1347
- package/src/suggest.mjs +1 -68
- package/src/symbols.mjs +1 -9
- package/MUZE_ALIGNMENT.md +0 -118
- package/src/bind.render.mjs +0 -694
- package/src/bind.transformers.mjs +0 -25
package/src/state.mjs
CHANGED
|
@@ -1,1347 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const MAP_READS_KEY = new Set(['get', 'has'])
|
|
4
|
-
const MAP_READS_ITERATION = new Set(['keys', 'values', 'entries', 'forEach', Symbol.iterator])
|
|
5
|
-
const MAP_WRITES = new Set(['set', 'delete', 'clear'])
|
|
6
|
-
const SET_WRITES = new Set(['add', 'delete', 'clear'])
|
|
7
|
-
const SET_ITERATION_PROPERTIES = {
|
|
8
|
-
entries: {},
|
|
9
|
-
forEach: {},
|
|
10
|
-
has: {},
|
|
11
|
-
keys: {},
|
|
12
|
-
values: {},
|
|
13
|
-
[Symbol.iterator]: {}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function isObjectLike(value) {
|
|
17
|
-
return value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Returns true when value is a SimplyFlow signal proxy.
|
|
22
|
-
*
|
|
23
|
-
* @param {*} value Value to inspect.
|
|
24
|
-
* @returns {boolean} True if value is a signal proxy, otherwise false.
|
|
25
|
-
* @throws {never} Does not intentionally throw.
|
|
26
|
-
*/
|
|
27
|
-
export function isSignal(value) {
|
|
28
|
-
return Boolean(isObjectLike(value) && value[DEP.SIGNAL])
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Returns the raw target for a signal, or value unchanged when it is not a signal.
|
|
33
|
-
*
|
|
34
|
-
* @param {*} value Signal or ordinary value.
|
|
35
|
-
* @returns {*} The signal target, or the original value.
|
|
36
|
-
* @throws {never} Does not intentionally throw.
|
|
37
|
-
*/
|
|
38
|
-
export function raw(value) {
|
|
39
|
-
return isSignal(value) ? value[DEP.XRAY] : value
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Returns the existing signal proxy for value, if one has been registered.
|
|
44
|
-
*
|
|
45
|
-
* @param {*} value Raw target or signal proxy.
|
|
46
|
-
* @returns {*} The existing signal proxy, the same signal when value is already a signal, or undefined.
|
|
47
|
-
* @throws {never} Does not intentionally throw.
|
|
48
|
-
*/
|
|
49
|
-
export function getSignal(value) {
|
|
50
|
-
return isSignal(value) ? value : signals.get(value)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function targetSignal(target) {
|
|
54
|
-
return signals.get(target)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readTarget(target, property) {
|
|
58
|
-
// Reflect.get() uses the proxy as the receiver for accessors. That breaks
|
|
59
|
-
// native Map/Set size getters and class getters that rely on private fields.
|
|
60
|
-
return target?.[property]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function bindMethod(target, receiver, value) {
|
|
64
|
-
if (
|
|
65
|
-
target instanceof HTMLElement
|
|
66
|
-
|| target instanceof Number
|
|
67
|
-
|| target instanceof String
|
|
68
|
-
|| target instanceof Boolean
|
|
69
|
-
) {
|
|
70
|
-
return value.bind(target)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// For user-defined classes, bind to the signal so method bodies remain
|
|
74
|
-
// reactive when they read or write public properties through `this`.
|
|
75
|
-
return value.bind(receiver)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function collectRemovedArrayValues(target, nextLength) {
|
|
79
|
-
const values = new Map()
|
|
80
|
-
if (!Array.isArray(target) || nextLength >= target.length) {
|
|
81
|
-
return values
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (let index = nextLength; index < target.length; index++) {
|
|
85
|
-
if (Object.hasOwn(target, index)) {
|
|
86
|
-
values.set(index, target[index])
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return values
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function addArrayLengthChanges(context, target, oldLength, removedValues = new Map()) {
|
|
93
|
-
if (!Array.isArray(target) || oldLength === target.length) {
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
context.set(DEP.LENGTH, { was: oldLength, now: target.length })
|
|
98
|
-
context.set(DEP.ITERATE, {})
|
|
99
|
-
|
|
100
|
-
// Directly shrinking .length deletes indexes without going through the
|
|
101
|
-
// proxy's delete trap. Notify listeners of those indexes explicitly.
|
|
102
|
-
for (const [index, oldValue] of removedValues) {
|
|
103
|
-
context.set(String(index), { delete: true, was: oldValue, now: undefined })
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function notifyContext(receiver, context) {
|
|
108
|
-
if (context.size) {
|
|
109
|
-
notifySet(receiver, context)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function wrapArrayMethod(target, property, receiver, value) {
|
|
114
|
-
return (...args) => {
|
|
115
|
-
const oldLength = target.length
|
|
116
|
-
|
|
117
|
-
// Native array methods must run with the proxy as `this`. That lets
|
|
118
|
-
// their internal get/set/delete operations pass through the proxy traps.
|
|
119
|
-
const result = value.apply(receiver, args)
|
|
120
|
-
|
|
121
|
-
if (oldLength !== target.length) {
|
|
122
|
-
notifySet(receiver, makeContext(DEP.LENGTH, { was: oldLength, now: target.length }))
|
|
123
|
-
}
|
|
124
|
-
return result
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function addMapWriteChanges(context, target, property, args, oldSize) {
|
|
129
|
-
if (property === 'set') {
|
|
130
|
-
const [key, nextValue] = args
|
|
131
|
-
const hadKey = target.has(key)
|
|
132
|
-
const oldValue = target.get(key)
|
|
133
|
-
|
|
134
|
-
return () => {
|
|
135
|
-
if (!hadKey || !Object.is(oldValue, nextValue)) {
|
|
136
|
-
context.set(key, { was: oldValue, now: nextValue })
|
|
137
|
-
// Existing value changes affect values(), entries(), forEach()
|
|
138
|
-
// and direct iteration. The current dependency model uses one
|
|
139
|
-
// iteration token, so keys() listeners are also conservatively
|
|
140
|
-
// notified until Map iteration dependencies are split further.
|
|
141
|
-
context.set(DEP.ITERATE, {})
|
|
142
|
-
}
|
|
143
|
-
if (!hadKey) {
|
|
144
|
-
context.set(DEP.SIZE, { was: oldSize, now: target.size })
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (property === 'delete') {
|
|
150
|
-
const [key] = args
|
|
151
|
-
const hadKey = target.has(key)
|
|
152
|
-
const oldValue = target.get(key)
|
|
153
|
-
|
|
154
|
-
return () => {
|
|
155
|
-
if (hadKey) {
|
|
156
|
-
context.set(key, { delete: true, was: oldValue, now: undefined })
|
|
157
|
-
context.set(DEP.SIZE, { was: oldSize, now: target.size })
|
|
158
|
-
context.set(DEP.ITERATE, {})
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (property === 'clear') {
|
|
164
|
-
const oldEntries = oldSize ? Array.from(target.entries()) : []
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
if (oldEntries.length) {
|
|
168
|
-
for (const [key, oldValue] of oldEntries) {
|
|
169
|
-
context.set(key, { delete: true, was: oldValue, now: undefined })
|
|
170
|
-
}
|
|
171
|
-
context.set(DEP.SIZE, { was: oldSize, now: target.size })
|
|
172
|
-
context.set(DEP.ITERATE, {})
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return () => {}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function wrapMapMethod(target, property, receiver, value) {
|
|
181
|
-
return (...args) => {
|
|
182
|
-
if (MAP_READS_KEY.has(property)) {
|
|
183
|
-
notifyGet(receiver, args[0])
|
|
184
|
-
}
|
|
185
|
-
if (MAP_READS_ITERATION.has(property)) {
|
|
186
|
-
notifyGet(receiver, DEP.ITERATE)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const oldSize = target.size
|
|
190
|
-
const context = new Map()
|
|
191
|
-
const addChanges = MAP_WRITES.has(property)
|
|
192
|
-
? addMapWriteChanges(context, target, property, args, oldSize)
|
|
193
|
-
: () => {}
|
|
194
|
-
const result = value.apply(target, args)
|
|
195
|
-
|
|
196
|
-
addChanges()
|
|
197
|
-
notifyContext(receiver, context)
|
|
198
|
-
return result
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function addSetWriteChanges(context, target, property, args, oldSize) {
|
|
203
|
-
const [value] = args
|
|
204
|
-
const hadValue = property === 'add' || property === 'delete'
|
|
205
|
-
? target.has(value)
|
|
206
|
-
: false
|
|
207
|
-
|
|
208
|
-
return () => {
|
|
209
|
-
const changed = property === 'clear'
|
|
210
|
-
? oldSize > 0
|
|
211
|
-
: target.size !== oldSize || (property === 'delete' && hadValue)
|
|
212
|
-
|
|
213
|
-
if (!changed) {
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
context.set(DEP.SIZE, { was: oldSize, now: target.size })
|
|
218
|
-
|
|
219
|
-
// Set.has(value) currently tracks at method level rather than per value.
|
|
220
|
-
// Notify all Set read methods after real writes so this remains correct,
|
|
221
|
-
// but suppress add(existing), delete(missing), and clear(empty).
|
|
222
|
-
for (const prop of Reflect.ownKeys(SET_ITERATION_PROPERTIES)) {
|
|
223
|
-
context.set(prop, {})
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function wrapSetMethod(target, property, receiver, value) {
|
|
229
|
-
return (...args) => {
|
|
230
|
-
const oldSize = target.size
|
|
231
|
-
const context = new Map()
|
|
232
|
-
const addChanges = SET_WRITES.has(property)
|
|
233
|
-
? addSetWriteChanges(context, target, property, args, oldSize)
|
|
234
|
-
: () => {}
|
|
235
|
-
const result = value.apply(target, args)
|
|
236
|
-
|
|
237
|
-
addChanges()
|
|
238
|
-
notifyContext(receiver, context)
|
|
239
|
-
return result
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function propertyValueChanged(descriptor, oldDescriptor, oldValue, newDescriptor, newValue) {
|
|
244
|
-
return (
|
|
245
|
-
(Object.hasOwn(descriptor, 'value') && !Object.is(oldValue, newValue))
|
|
246
|
-
|| (Object.hasOwn(descriptor, 'get') && oldDescriptor?.get !== newDescriptor?.get)
|
|
247
|
-
|| (Object.hasOwn(descriptor, 'set') && oldDescriptor?.set !== newDescriptor?.set)
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const signalHandler = {
|
|
252
|
-
get(target, property, receiver) {
|
|
253
|
-
const value = readTarget(target, property)
|
|
254
|
-
notifyGet(receiver, property)
|
|
255
|
-
|
|
256
|
-
if (typeof value === 'function') {
|
|
257
|
-
if (Array.isArray(target)) {
|
|
258
|
-
return wrapArrayMethod(target, property, receiver, value)
|
|
259
|
-
}
|
|
260
|
-
if (target instanceof Map) {
|
|
261
|
-
return wrapMapMethod(target, property, receiver, value)
|
|
262
|
-
}
|
|
263
|
-
if (target instanceof Set) {
|
|
264
|
-
return wrapSetMethod(target, property, receiver, value)
|
|
265
|
-
}
|
|
266
|
-
return bindMethod(target, receiver, value)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return isObjectLike(value) ? signal(value) : value
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
set(target, property, value, receiver) {
|
|
273
|
-
const hadOwn = Object.hasOwn(target, property)
|
|
274
|
-
const oldLength = Array.isArray(target) ? target.length : undefined
|
|
275
|
-
const removedValues = property === DEP.LENGTH
|
|
276
|
-
? collectRemovedArrayValues(target, Number(value))
|
|
277
|
-
: new Map()
|
|
278
|
-
const oldValue = target[property]
|
|
279
|
-
|
|
280
|
-
target[property] = value
|
|
281
|
-
|
|
282
|
-
const hasOwn = Object.hasOwn(target, property)
|
|
283
|
-
const newValue = target[property]
|
|
284
|
-
const context = new Map()
|
|
285
|
-
|
|
286
|
-
if (!Object.is(oldValue, newValue) || (!hadOwn && hasOwn)) {
|
|
287
|
-
context.set(property, { was: oldValue, now: newValue })
|
|
288
|
-
}
|
|
289
|
-
if (!hadOwn && hasOwn) {
|
|
290
|
-
context.set(DEP.ITERATE, {})
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
addArrayLengthChanges(context, target, oldLength, removedValues)
|
|
294
|
-
notifyContext(receiver, context)
|
|
295
|
-
return true
|
|
296
|
-
},
|
|
297
|
-
|
|
298
|
-
has(target, property) {
|
|
299
|
-
// The has trap has no receiver argument. Look up the stable proxy so
|
|
300
|
-
// `property in signal` can still be tracked reactively.
|
|
301
|
-
const receiver = targetSignal(target)
|
|
302
|
-
if (receiver) {
|
|
303
|
-
notifyGet(receiver, property)
|
|
304
|
-
}
|
|
305
|
-
return Reflect.has(target, property)
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
deleteProperty(target, property) {
|
|
309
|
-
const hadOwn = Object.hasOwn(target, property)
|
|
310
|
-
if (!hadOwn) {
|
|
311
|
-
return true
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const oldValue = target[property]
|
|
315
|
-
const oldLength = Array.isArray(target) ? target.length : undefined
|
|
316
|
-
const result = Reflect.deleteProperty(target, property)
|
|
317
|
-
if (!result) {
|
|
318
|
-
return result
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const receiver = targetSignal(target)
|
|
322
|
-
const context = makeContext(property, { delete: true, was: oldValue, now: undefined })
|
|
323
|
-
context.set(DEP.ITERATE, { delete: true, property })
|
|
324
|
-
addArrayLengthChanges(context, target, oldLength)
|
|
325
|
-
notifySet(receiver, context)
|
|
326
|
-
return result
|
|
327
|
-
},
|
|
328
|
-
|
|
329
|
-
defineProperty(target, property, descriptor) {
|
|
330
|
-
const hadOwn = Object.hasOwn(target, property)
|
|
331
|
-
const oldDescriptor = Object.getOwnPropertyDescriptor(target, property)
|
|
332
|
-
const oldValue = target[property]
|
|
333
|
-
const oldLength = Array.isArray(target) ? target.length : undefined
|
|
334
|
-
const removedValues = property === DEP.LENGTH && Object.hasOwn(descriptor, 'value')
|
|
335
|
-
? collectRemovedArrayValues(target, Number(descriptor.value))
|
|
336
|
-
: new Map()
|
|
337
|
-
|
|
338
|
-
const result = Reflect.defineProperty(target, property, descriptor)
|
|
339
|
-
if (!result) {
|
|
340
|
-
return result
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const hasOwn = Object.hasOwn(target, property)
|
|
344
|
-
const newDescriptor = Object.getOwnPropertyDescriptor(target, property)
|
|
345
|
-
const newValue = target[property]
|
|
346
|
-
const context = new Map()
|
|
347
|
-
|
|
348
|
-
if (!hadOwn && hasOwn) {
|
|
349
|
-
context.set(property, { was: oldValue, now: newValue })
|
|
350
|
-
context.set(DEP.ITERATE, {})
|
|
351
|
-
} else if (hadOwn && hasOwn) {
|
|
352
|
-
if (propertyValueChanged(descriptor, oldDescriptor, oldValue, newDescriptor, newValue)) {
|
|
353
|
-
context.set(property, { was: oldValue, now: newValue })
|
|
354
|
-
}
|
|
355
|
-
if (oldDescriptor?.enumerable !== newDescriptor?.enumerable) {
|
|
356
|
-
context.set(DEP.ITERATE, {})
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
addArrayLengthChanges(context, target, oldLength, removedValues)
|
|
361
|
-
notifyContext(targetSignal(target), context)
|
|
362
|
-
return result
|
|
363
|
-
},
|
|
364
|
-
|
|
365
|
-
ownKeys(target) {
|
|
366
|
-
const receiver = targetSignal(target)
|
|
367
|
-
notifyGet(receiver, DEP.ITERATE)
|
|
368
|
-
return Reflect.ownKeys(target)
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Low-level registry for signal proxies and effect result signals.
|
|
374
|
-
*
|
|
375
|
-
* @type {WeakMap<object|Function, Proxy>}
|
|
376
|
-
* @returns {WeakMap<object|Function, Proxy>} Maps raw targets to signals, and effect functions to result signals.
|
|
377
|
-
* @throws {never} Reading this property does not throw.
|
|
378
|
-
* @deprecated Prefer createSignal(), getSignal(), registerSignal(), isSignal(), and raw().
|
|
379
|
-
*/
|
|
380
|
-
export const signals = new WeakMap()
|
|
381
|
-
|
|
382
|
-
function assertSignalTarget(value, name) {
|
|
383
|
-
if (!isObjectLike(value)) {
|
|
384
|
-
throw new TypeError(
|
|
385
|
-
`simplyflow/state: ${name}() expects an object, array, Map, Set, class instance, function, or DOM node; received ${typeof value}`
|
|
386
|
-
)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function assertProxyHandler(handler, name) {
|
|
391
|
-
if (!handler || typeof handler !== 'object') {
|
|
392
|
-
throw new TypeError(`simplyflow/state: ${name}() expects a Proxy handler object`)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function signalProxyHandler(handler) {
|
|
397
|
-
// All signal implementations must answer these two private symbol reads in
|
|
398
|
-
// the same way. Keeping that boilerplate here lets custom signal handlers
|
|
399
|
-
// focus on their own get/set/observe behavior.
|
|
400
|
-
return {
|
|
401
|
-
...handler,
|
|
402
|
-
get(target, property, receiver) {
|
|
403
|
-
if (property === DEP.XRAY) {
|
|
404
|
-
return target
|
|
405
|
-
}
|
|
406
|
-
if (property === DEP.SIGNAL) {
|
|
407
|
-
return true
|
|
408
|
-
}
|
|
409
|
-
if (handler.get) {
|
|
410
|
-
return handler.get(target, property, receiver)
|
|
411
|
-
}
|
|
412
|
-
return readTarget(target, property)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Registers a custom signal proxy for a raw target.
|
|
419
|
-
*
|
|
420
|
-
* @param {object|Function} target Raw object, function, class instance, collection, or DOM node to register.
|
|
421
|
-
* @param {Proxy} proxy Signal proxy that represents target.
|
|
422
|
-
* @returns {Proxy} The registered proxy.
|
|
423
|
-
* @throws {TypeError} If target is not object-like or proxy is not a signal.
|
|
424
|
-
* @throws {Error} If target is already registered with a different signal.
|
|
425
|
-
*/
|
|
426
|
-
export function registerSignal(target, proxy) {
|
|
427
|
-
const rawTarget = raw(target)
|
|
428
|
-
assertSignalTarget(rawTarget, 'registerSignal')
|
|
429
|
-
|
|
430
|
-
if (!isSignal(proxy)) {
|
|
431
|
-
throw new TypeError('simplyflow/state: registerSignal() expects a signal proxy')
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const existing = signals.get(rawTarget)
|
|
435
|
-
if (existing && existing !== proxy) {
|
|
436
|
-
throw new Error('simplyflow/state: registerSignal() target already has a different signal')
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
signals.set(rawTarget, proxy)
|
|
440
|
-
return proxy
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Creates or returns a signal proxy using a custom Proxy handler.
|
|
445
|
-
*
|
|
446
|
-
* @param {object|Function} target Raw object, function, class instance, collection, or DOM node to wrap.
|
|
447
|
-
* @param {ProxyHandler<object>} [handler={}] Custom proxy traps for the signal.
|
|
448
|
-
* @param {Function} [init] Optional initializer called once with (target, proxy).
|
|
449
|
-
* @returns {Proxy} Existing or newly created signal proxy for target.
|
|
450
|
-
* @throws {TypeError} If target is not object-like, handler is not an object, or init is not a function.
|
|
451
|
-
* @throws {*} Re-throws errors from init.
|
|
452
|
-
*/
|
|
453
|
-
export function createSignal(target, handler = {}, init) {
|
|
454
|
-
assertSignalTarget(target, 'createSignal')
|
|
455
|
-
assertProxyHandler(handler, 'createSignal')
|
|
456
|
-
if (init !== undefined && typeof init !== 'function') {
|
|
457
|
-
throw new TypeError('simplyflow/state: createSignal() expects init to be a function')
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (isSignal(target)) {
|
|
461
|
-
return target
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const existing = getSignal(target)
|
|
465
|
-
if (existing) {
|
|
466
|
-
return existing
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const proxy = new Proxy(target, signalProxyHandler(handler))
|
|
470
|
-
registerSignal(target, proxy)
|
|
471
|
-
init?.(target, proxy)
|
|
472
|
-
return proxy
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Creates a transparent reactive proxy for an object, collection, class instance, DOM node, or function.
|
|
477
|
-
*
|
|
478
|
-
* @param {object|Function} [value={}] Target to wrap.
|
|
479
|
-
* @returns {Proxy} Existing or newly created signal proxy for value.
|
|
480
|
-
* @throws {TypeError} If value is not object-like.
|
|
481
|
-
*/
|
|
482
|
-
export function signal(value = {}) {
|
|
483
|
-
if (!isObjectLike(value)) {
|
|
484
|
-
throw new TypeError(
|
|
485
|
-
`simplyflow/state: signal() expects an object, array, Map, Set, class instance, or function; received ${typeof value}`
|
|
486
|
-
)
|
|
487
|
-
}
|
|
488
|
-
return createSignal(value, signalHandler)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
let tracers = []
|
|
492
|
-
let tracing = false
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Runs a traced function, or returns effects currently depending on a signal property.
|
|
496
|
-
*
|
|
497
|
-
* @param {Function|Proxy} target Function to run with tracing enabled, or signal to inspect.
|
|
498
|
-
* @param {string|symbol|number} [prop] Signal property whose listeners should be returned.
|
|
499
|
-
* @returns {*} Result of target() for traced functions, or an array of listener descriptions.
|
|
500
|
-
* @throws {TypeError} If inspecting listeners and target is not a signal.
|
|
501
|
-
* @throws {*} Re-throws errors from target() when tracing a function.
|
|
502
|
-
*/
|
|
503
|
-
export function trace(target, prop) {
|
|
504
|
-
if (typeof target === 'function') {
|
|
505
|
-
tracing = true
|
|
506
|
-
try {
|
|
507
|
-
return target()
|
|
508
|
-
} finally {
|
|
509
|
-
tracing = false
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (!isSignal(target)) {
|
|
514
|
-
throw new TypeError('simplyflow/state: trace() expects either a function or a signal')
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return getListeners(target, prop).map(listener => ({
|
|
518
|
-
effect: listener.effectType,
|
|
519
|
-
fn: listener.effectFunction,
|
|
520
|
-
signal: signals.get(listener.effectFunction)
|
|
521
|
-
}))
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Adds an observer for dependency tracking events emitted inside trace(fn).
|
|
526
|
-
*
|
|
527
|
-
* @param {{get?: Function, set?: Function}} tracer Object with get and/or set callback functions.
|
|
528
|
-
* @returns {void}
|
|
529
|
-
* @throws {TypeError} If tracer is not an object.
|
|
530
|
-
* @throws {Error} If tracer has no callbacks, or if a provided callback is not a function.
|
|
531
|
-
*/
|
|
532
|
-
export function addTracer(tracer) {
|
|
533
|
-
if (!tracer || typeof tracer !== 'object') {
|
|
534
|
-
throw new TypeError('simplyflow/state: addTracer() expects a tracer object')
|
|
535
|
-
}
|
|
536
|
-
if (!tracer.get && !tracer.set) {
|
|
537
|
-
throw new Error('simplyflow/state: addTracer: missing "get" or "set" property in tracer')
|
|
538
|
-
}
|
|
539
|
-
if (tracer.get && typeof tracer.get !== 'function') {
|
|
540
|
-
throw new Error('simplyflow/state: addTracer: "get" is not a function')
|
|
541
|
-
}
|
|
542
|
-
if (tracer.set && typeof tracer.set !== 'function') {
|
|
543
|
-
throw new Error('simplyflow/state: addTracer: "set" is not a function')
|
|
544
|
-
}
|
|
545
|
-
tracers.push(tracer)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function callTracers(kind, ...params) {
|
|
549
|
-
for (const tracer of tracers) {
|
|
550
|
-
tracer[kind]?.(...params)
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
let batchedListeners = new Set()
|
|
555
|
-
let batchDepth = 0
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Triggers effects that depend on the changed signal properties in context.
|
|
559
|
-
*
|
|
560
|
-
* @param {Proxy} self Signal whose properties changed.
|
|
561
|
-
* @param {Map<string|symbol|number, object>} [context=new Map()] Change map, usually created with makeContext().
|
|
562
|
-
* @returns {void}
|
|
563
|
-
* @throws {TypeError} If self is not a signal or context is not a Map.
|
|
564
|
-
*/
|
|
565
|
-
export function notifySet(self, context = new Map()) {
|
|
566
|
-
if (!isSignal(self)) {
|
|
567
|
-
throw new TypeError('simplyflow/state: notifySet() expects a signal as first argument')
|
|
568
|
-
}
|
|
569
|
-
if (!(context instanceof Map)) {
|
|
570
|
-
throw new TypeError('simplyflow/state: notifySet() expects context to be a Map; use makeContext()')
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const listeners = new Set()
|
|
574
|
-
context.forEach((change, property) => {
|
|
575
|
-
for (const listener of listenersFor(self, property)) {
|
|
576
|
-
// Avoid makeContext() here: notifySet() is a hot path and this loop
|
|
577
|
-
// can run once per changed property per listener. Writing directly
|
|
578
|
-
// to the listener context keeps object/Map keys intact and avoids
|
|
579
|
-
// creating a short-lived Map for every listener notification.
|
|
580
|
-
addContextChange(listener, property, change)
|
|
581
|
-
listeners.add(listener)
|
|
582
|
-
}
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
if (!listeners.size) {
|
|
586
|
-
return
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (batchDepth) {
|
|
590
|
-
for (const listener of listeners) {
|
|
591
|
-
batchedListeners.add(listener)
|
|
592
|
-
}
|
|
593
|
-
return
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
runListeners(listeners, self, context)
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Creates a normalized change context map for notifySet().
|
|
601
|
-
*
|
|
602
|
-
* @param {Map|object|string|symbol|number} property Property name, property-to-change object, or existing context Map.
|
|
603
|
-
* @param {object} [change] Change metadata when property names a single changed property.
|
|
604
|
-
* @returns {Map<string|symbol|number, object>} Normalized change context.
|
|
605
|
-
* @throws {never} Does not intentionally throw.
|
|
606
|
-
*/
|
|
607
|
-
export function makeContext(property, change) {
|
|
608
|
-
const context = new Map()
|
|
609
|
-
|
|
610
|
-
if (property instanceof Map) {
|
|
611
|
-
property.forEach((change, prop) => context.set(prop, change))
|
|
612
|
-
return context
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (property !== null && typeof property === 'object') {
|
|
616
|
-
for (const prop of Reflect.ownKeys(property)) {
|
|
617
|
-
context.set(prop, property[prop])
|
|
618
|
-
}
|
|
619
|
-
} else {
|
|
620
|
-
context.set(property, change)
|
|
621
|
-
}
|
|
622
|
-
return context
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function addContextChange(listener, property, change) {
|
|
626
|
-
if (!listener.context) {
|
|
627
|
-
listener.context = new Map()
|
|
628
|
-
}
|
|
629
|
-
listener.context.set(property, change)
|
|
630
|
-
listener.needsUpdate = true
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function clearContext(listener) {
|
|
634
|
-
delete listener.context
|
|
635
|
-
delete listener.needsUpdate
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Records a dependency on self[property] for the currently running effect.
|
|
640
|
-
*
|
|
641
|
-
* @param {Proxy} self Signal being read.
|
|
642
|
-
* @param {string|symbol|number} property Property being read.
|
|
643
|
-
* @returns {void}
|
|
644
|
-
* @throws {never} Does not intentionally throw.
|
|
645
|
-
*/
|
|
646
|
-
export function notifyGet(self, property) {
|
|
647
|
-
const currentCompute = computeStack[computeStack.length - 1]
|
|
648
|
-
if (!currentCompute || currentCompute.skipDependency?.(self, property)) {
|
|
649
|
-
return
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (tracing && tracers.length) {
|
|
653
|
-
callTracers('get', self, property)
|
|
654
|
-
}
|
|
655
|
-
setListeners(self, property, currentCompute)
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const listenersMap = new WeakMap()
|
|
659
|
-
const computeMap = new WeakMap()
|
|
660
|
-
|
|
661
|
-
const emptyListeners = new Set()
|
|
662
|
-
|
|
663
|
-
function listenersFor(self, property) {
|
|
664
|
-
return listenersMap.get(self)?.get(property) || emptyListeners
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function getListeners(self, property) {
|
|
668
|
-
return Array.from(listenersFor(self, property))
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function setListeners(self, property, compute) {
|
|
672
|
-
if (!listenersMap.has(self)) {
|
|
673
|
-
listenersMap.set(self, new Map())
|
|
674
|
-
}
|
|
675
|
-
const listeners = listenersMap.get(self)
|
|
676
|
-
if (!listeners.has(property)) {
|
|
677
|
-
listeners.set(property, new Set())
|
|
678
|
-
}
|
|
679
|
-
listeners.get(property).add(compute)
|
|
680
|
-
|
|
681
|
-
if (!computeMap.has(compute)) {
|
|
682
|
-
computeMap.set(compute, new Map())
|
|
683
|
-
}
|
|
684
|
-
const dependencies = computeMap.get(compute)
|
|
685
|
-
if (!dependencies.has(property)) {
|
|
686
|
-
dependencies.set(property, new Set())
|
|
687
|
-
}
|
|
688
|
-
dependencies.get(property).add(self)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function clearListeners(compute) {
|
|
692
|
-
const dependencies = computeMap.get(compute)
|
|
693
|
-
if (!dependencies) {
|
|
694
|
-
return
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
dependencies.forEach((signals, property) => {
|
|
698
|
-
signals.forEach(signal => {
|
|
699
|
-
const listeners = listenersMap.get(signal)
|
|
700
|
-
listeners?.get(property)?.delete(compute)
|
|
701
|
-
})
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
computeMap.delete(compute)
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const computeStack = []
|
|
708
|
-
const effectStack = []
|
|
709
|
-
const signalStack = []
|
|
710
|
-
const effectMap = new WeakMap()
|
|
711
|
-
|
|
712
|
-
function assertFunction(fn, name) {
|
|
713
|
-
if (typeof fn !== 'function') {
|
|
714
|
-
throw new TypeError(`simplyflow/state: ${name}() expects a function`)
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function assertNotRecursive(fn) {
|
|
719
|
-
if (effectStack.includes(fn)) {
|
|
720
|
-
throw new Error('Recursive update() call', { cause: fn })
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function effectSignal(fn) {
|
|
725
|
-
let connectedSignal = signals.get(fn)
|
|
726
|
-
if (!connectedSignal) {
|
|
727
|
-
connectedSignal = signal({ current: null })
|
|
728
|
-
signals.set(fn, connectedSignal)
|
|
729
|
-
}
|
|
730
|
-
return connectedSignal
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function setEffectResult(connectedSignal, result) {
|
|
734
|
-
if (result instanceof Promise) {
|
|
735
|
-
result.then(value => {
|
|
736
|
-
connectedSignal.current = value
|
|
737
|
-
})
|
|
738
|
-
} else {
|
|
739
|
-
connectedSignal.current = result
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function runTracked(compute, connectedSignal, fn, effectType, args = [compute, computeStack, signalStack]) {
|
|
744
|
-
if (signalStack.includes(connectedSignal)) {
|
|
745
|
-
throw new Error('Cyclical dependency in update() call', { cause: fn })
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
clearListeners(compute)
|
|
749
|
-
compute.effectFunction = fn
|
|
750
|
-
compute.effectType = effectType
|
|
751
|
-
computeStack.push(compute)
|
|
752
|
-
signalStack.push(connectedSignal)
|
|
753
|
-
|
|
754
|
-
let result
|
|
755
|
-
try {
|
|
756
|
-
result = fn(...args)
|
|
757
|
-
} finally {
|
|
758
|
-
computeStack.pop()
|
|
759
|
-
signalStack.pop()
|
|
760
|
-
setEffectResult(connectedSignal, result)
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function runListeners(listeners, signal, context) {
|
|
765
|
-
const currentEffect = computeStack[computeStack.length - 1]
|
|
766
|
-
|
|
767
|
-
for (const listener of listeners) {
|
|
768
|
-
if (listener !== currentEffect && listener?.needsUpdate) {
|
|
769
|
-
if (listener.scheduleClock) {
|
|
770
|
-
listener.scheduleClock()
|
|
771
|
-
} else {
|
|
772
|
-
if (signal && tracing && tracers.length) {
|
|
773
|
-
callTracers('set', signal, context, listener)
|
|
774
|
-
}
|
|
775
|
-
listener()
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
clearContext(listener)
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Runs fn immediately and reruns it synchronously when any signal property it reads changes.
|
|
784
|
-
*
|
|
785
|
-
* @param {Function} fn Effect function to run and track.
|
|
786
|
-
* @returns {Proxy} Signal whose current property contains the latest effect result.
|
|
787
|
-
* @throws {TypeError} If fn is not a function.
|
|
788
|
-
* @throws {Error} If fn is already running recursively or creates a cyclic dependency.
|
|
789
|
-
* @throws {*} Re-throws errors from fn during the initial run.
|
|
790
|
-
*/
|
|
791
|
-
export function effect(fn) {
|
|
792
|
-
assertFunction(fn, 'effect')
|
|
793
|
-
assertNotRecursive(fn)
|
|
794
|
-
effectStack.push(fn)
|
|
795
|
-
|
|
796
|
-
const connectedSignal = effectSignal(fn)
|
|
797
|
-
const compute = function computeEffect() {
|
|
798
|
-
runTracked(compute, connectedSignal, fn, effect)
|
|
799
|
-
}
|
|
800
|
-
compute.fn = fn
|
|
801
|
-
effectMap.set(connectedSignal, compute)
|
|
802
|
-
|
|
803
|
-
compute()
|
|
804
|
-
return connectedSignal
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Stops an effect, clears its dependencies, and releases its reusable function mapping.
|
|
809
|
-
*
|
|
810
|
-
* @param {Proxy} connectedSignal Signal returned by effect(), throttledEffect(), or clockEffect().
|
|
811
|
-
* @returns {void}
|
|
812
|
-
* @throws {TypeError} If connectedSignal is not a signal.
|
|
813
|
-
*/
|
|
814
|
-
export function destroy(connectedSignal) {
|
|
815
|
-
if (!isSignal(connectedSignal)) {
|
|
816
|
-
throw new TypeError('simplyflow/state: destroy() expects an effect signal')
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const compute = effectMap.get(connectedSignal)
|
|
820
|
-
if (!compute) {
|
|
821
|
-
return
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
compute.destroy?.()
|
|
825
|
-
clearListeners(compute)
|
|
826
|
-
|
|
827
|
-
if (compute.fn) {
|
|
828
|
-
signals.delete(compute.fn)
|
|
829
|
-
const index = effectStack.findIndex(fn => fn === compute.fn)
|
|
830
|
-
if (index !== -1) {
|
|
831
|
-
effectStack.splice(index, 1)
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
effectMap.delete(connectedSignal)
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Runs fn while deferring effect execution until the outermost batch finishes.
|
|
840
|
-
*
|
|
841
|
-
* @param {Function} fn Function to run inside the batch.
|
|
842
|
-
* @returns {*} The value returned by fn.
|
|
843
|
-
* @throws {TypeError} If fn is not a function.
|
|
844
|
-
* @throws {*} Re-throws errors from fn.
|
|
845
|
-
*/
|
|
846
|
-
export function batch(fn) {
|
|
847
|
-
assertFunction(fn, 'batch')
|
|
848
|
-
batchDepth++
|
|
849
|
-
|
|
850
|
-
let result
|
|
851
|
-
try {
|
|
852
|
-
result = fn()
|
|
853
|
-
} finally {
|
|
854
|
-
const finish = () => {
|
|
855
|
-
batchDepth--
|
|
856
|
-
if (!batchDepth) {
|
|
857
|
-
runBatchedListeners()
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (result instanceof Promise) {
|
|
862
|
-
result.then(finish, finish)
|
|
863
|
-
} else {
|
|
864
|
-
finish()
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
return result
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function runBatchedListeners() {
|
|
871
|
-
const listeners = batchedListeners
|
|
872
|
-
batchedListeners = new Set()
|
|
873
|
-
|
|
874
|
-
// Batched clocked dependency changes must be marked before clock ticks are
|
|
875
|
-
// flushed. Otherwise batch(() => { clock.time++; source.value++ }) would see
|
|
876
|
-
// the tick first and leave the source change pending until the next tick.
|
|
877
|
-
const clocked = new Set()
|
|
878
|
-
const ready = new Set()
|
|
879
|
-
for (const listener of listeners) {
|
|
880
|
-
if (listener.scheduleClock) {
|
|
881
|
-
clocked.add(listener)
|
|
882
|
-
} else {
|
|
883
|
-
ready.add(listener)
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
runListeners(clocked)
|
|
888
|
-
runListeners(ready)
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Runs fn as an effect, throttling reruns to at most once per throttleTime milliseconds.
|
|
893
|
-
*
|
|
894
|
-
* @param {Function} fn Effect function to run and track.
|
|
895
|
-
* @param {number} throttleTime Minimum time in milliseconds between reruns after the initial run.
|
|
896
|
-
* @returns {Proxy} Signal whose current property contains the latest effect result.
|
|
897
|
-
* @throws {TypeError} If fn is not a function or throttleTime is not a non-negative finite number.
|
|
898
|
-
* @throws {Error} If fn is already running recursively or creates a cyclic dependency.
|
|
899
|
-
* @throws {*} Re-throws errors from fn during the initial run.
|
|
900
|
-
*/
|
|
901
|
-
export function throttledEffect(fn, throttleTime) {
|
|
902
|
-
assertFunction(fn, 'throttledEffect')
|
|
903
|
-
if (!Number.isFinite(throttleTime) || throttleTime < 0) {
|
|
904
|
-
throw new TypeError('simplyflow/state: throttledEffect() expects throttleTime to be a non-negative number')
|
|
905
|
-
}
|
|
906
|
-
assertNotRecursive(fn)
|
|
907
|
-
effectStack.push(fn)
|
|
908
|
-
|
|
909
|
-
const connectedSignal = effectSignal(fn)
|
|
910
|
-
let throttledUntil = 0
|
|
911
|
-
let hasChange = true
|
|
912
|
-
let timeout = null
|
|
913
|
-
|
|
914
|
-
const compute = function computeEffect() {
|
|
915
|
-
const now = Date.now()
|
|
916
|
-
if (throttledUntil > now) {
|
|
917
|
-
hasChange = true
|
|
918
|
-
schedule()
|
|
919
|
-
return
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
runTracked(compute, connectedSignal, fn, throttledEffect)
|
|
923
|
-
hasChange = false
|
|
924
|
-
throttledUntil = Date.now() + throttleTime
|
|
925
|
-
schedule()
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function schedule() {
|
|
929
|
-
if (timeout) {
|
|
930
|
-
return
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const delay = Math.max(0, throttledUntil - Date.now())
|
|
934
|
-
timeout = globalThis.setTimeout(() => {
|
|
935
|
-
timeout = null
|
|
936
|
-
if (hasChange) {
|
|
937
|
-
compute()
|
|
938
|
-
}
|
|
939
|
-
}, delay)
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
compute.fn = fn
|
|
943
|
-
compute.destroy = () => {
|
|
944
|
-
if (timeout) {
|
|
945
|
-
globalThis.clearTimeout(timeout)
|
|
946
|
-
timeout = null
|
|
947
|
-
}
|
|
948
|
-
hasChange = false
|
|
949
|
-
}
|
|
950
|
-
effectMap.set(connectedSignal, compute)
|
|
951
|
-
|
|
952
|
-
compute()
|
|
953
|
-
return connectedSignal
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
const clockQueues = new WeakMap()
|
|
957
|
-
|
|
958
|
-
function readClockTime(clock) {
|
|
959
|
-
return raw(clock).time
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function getClockQueue(clock) {
|
|
963
|
-
if (!clockQueues.has(clock)) {
|
|
964
|
-
const queue = {
|
|
965
|
-
clock,
|
|
966
|
-
effects: new Set(),
|
|
967
|
-
pending: new Set(),
|
|
968
|
-
time: readClockTime(clock)
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// A clock has exactly one listener on `.time`. Ordinary dependency
|
|
972
|
-
// changes add clock effects to `pending`; the shared tick listener only
|
|
973
|
-
// flushes that pending set after time increases. This avoids waking every
|
|
974
|
-
// clockEffect on every clock tick just to discover most of them have no
|
|
975
|
-
// pending changes.
|
|
976
|
-
queue.tick = function tickClockEffects() {
|
|
977
|
-
const time = readClockTime(clock)
|
|
978
|
-
if (time <= queue.time) {
|
|
979
|
-
return
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
queue.time = time
|
|
983
|
-
const pending = Array.from(queue.pending)
|
|
984
|
-
queue.pending.clear()
|
|
985
|
-
|
|
986
|
-
for (const compute of pending) {
|
|
987
|
-
compute.clockPending = false
|
|
988
|
-
if (queue.effects.has(compute)) {
|
|
989
|
-
compute()
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
queue.tick.effectFunction = queue.tick
|
|
994
|
-
queue.tick.effectType = clockEffect
|
|
995
|
-
setListeners(clock, 'time', queue.tick)
|
|
996
|
-
clockQueues.set(clock, queue)
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
return clockQueues.get(clock)
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function detachClockEffect(compute) {
|
|
1003
|
-
const queue = compute.clockQueue
|
|
1004
|
-
if (!queue) {
|
|
1005
|
-
return
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
queue.pending.delete(compute)
|
|
1009
|
-
queue.effects.delete(compute)
|
|
1010
|
-
if (!queue.effects.size) {
|
|
1011
|
-
clearListeners(queue.tick)
|
|
1012
|
-
clockQueues.delete(queue.clock)
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/**
|
|
1017
|
-
* Tracks fn like an effect, but recomputes only after clock.time advances.
|
|
1018
|
-
*
|
|
1019
|
-
* @param {Function} fn Effect function to run and track.
|
|
1020
|
-
* @param {{time: number}} clock Clock object controlling when pending changes recompute.
|
|
1021
|
-
* @returns {Proxy} Signal whose current property contains the latest effect result.
|
|
1022
|
-
* @throws {TypeError} If fn is not a function or clock lacks a numeric time property.
|
|
1023
|
-
* @throws {*} Re-throws errors from fn during the initial run.
|
|
1024
|
-
*/
|
|
1025
|
-
export function clockEffect(fn, clock) {
|
|
1026
|
-
assertFunction(fn, 'clockEffect')
|
|
1027
|
-
if (!clock || typeof clock !== 'object' || typeof raw(clock).time !== 'number') {
|
|
1028
|
-
throw new TypeError('simplyflow/state: clockEffect() expects a clock object with a numeric .time property')
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const clockSignal = isSignal(clock) ? clock : signal(raw(clock))
|
|
1032
|
-
const connectedSignal = effectSignal(fn)
|
|
1033
|
-
const queue = getClockQueue(clockSignal)
|
|
1034
|
-
|
|
1035
|
-
const compute = function computeEffect() {
|
|
1036
|
-
clearListeners(compute)
|
|
1037
|
-
compute.effectFunction = fn
|
|
1038
|
-
compute.effectType = clockEffect
|
|
1039
|
-
computeStack.push(compute)
|
|
1040
|
-
|
|
1041
|
-
let result
|
|
1042
|
-
try {
|
|
1043
|
-
result = fn(compute, computeStack)
|
|
1044
|
-
} finally {
|
|
1045
|
-
computeStack.pop()
|
|
1046
|
-
setEffectResult(connectedSignal, result)
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
compute.fn = fn
|
|
1051
|
-
compute.clockQueue = queue
|
|
1052
|
-
compute.skipDependency = (self, property) => self === clockSignal && property === 'time'
|
|
1053
|
-
compute.scheduleClock = () => {
|
|
1054
|
-
if (!compute.clockPending) {
|
|
1055
|
-
compute.clockPending = true
|
|
1056
|
-
queue.pending.add(compute)
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
compute.destroy = () => detachClockEffect(compute)
|
|
1060
|
-
|
|
1061
|
-
queue.effects.add(compute)
|
|
1062
|
-
effectMap.set(connectedSignal, compute)
|
|
1063
|
-
|
|
1064
|
-
compute()
|
|
1065
|
-
return connectedSignal
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/**
|
|
1069
|
-
* Runs fn without recording signal reads as dependencies for the current effect.
|
|
1070
|
-
*
|
|
1071
|
-
* @param {Function} fn Function to run without dependency tracking.
|
|
1072
|
-
* @returns {*} The value returned by fn.
|
|
1073
|
-
* @throws {TypeError} If fn is not a function.
|
|
1074
|
-
* @throws {*} Re-throws errors from fn.
|
|
1075
|
-
*/
|
|
1076
|
-
export function untracked(fn) {
|
|
1077
|
-
assertFunction(fn, 'untracked')
|
|
1078
|
-
const index = computeStack.length - 1
|
|
1079
|
-
const current = computeStack[index]
|
|
1080
|
-
computeStack[index] = false
|
|
1081
|
-
try {
|
|
1082
|
-
return fn()
|
|
1083
|
-
} finally {
|
|
1084
|
-
computeStack[index] = current
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
function cloneOptions(options) {
|
|
1089
|
-
if (typeof options === 'boolean') {
|
|
1090
|
-
return { deep: options }
|
|
1091
|
-
}
|
|
1092
|
-
if (options === undefined) {
|
|
1093
|
-
return { deep: true }
|
|
1094
|
-
}
|
|
1095
|
-
if (!options || typeof options !== 'object') {
|
|
1096
|
-
throw new TypeError('simplyflow/state: clone() expects options to be a boolean or object')
|
|
1097
|
-
}
|
|
1098
|
-
return { deep: options.deep !== false }
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
function typeName(value) {
|
|
1102
|
-
return value?.constructor?.name || Object.prototype.toString.call(value).slice(8, -1)
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function isPlainObject(value) {
|
|
1106
|
-
const prototype = Object.getPrototypeOf(value)
|
|
1107
|
-
return prototype === Object.prototype || prototype === null
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function isTypedArray(value) {
|
|
1111
|
-
return ArrayBuffer.isView(value) && !(value instanceof DataView)
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
function isIntegerKey(property) {
|
|
1115
|
-
if (typeof property !== 'string' || property === '') {
|
|
1116
|
-
return false
|
|
1117
|
-
}
|
|
1118
|
-
const index = Number(property)
|
|
1119
|
-
return Number.isInteger(index) && index >= 0 && String(index) === property
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function hasToClone(value) {
|
|
1123
|
-
return typeof value.toClone === 'function'
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
function cannotClone(value, path) {
|
|
1127
|
-
throw new TypeError(
|
|
1128
|
-
`simplyflow/state: clone() cannot clone ${typeName(value)} at ${path}; add a toClone() method for custom objects`
|
|
1129
|
-
)
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
function cloneDescriptorProperties(source, result, cloneValue, skip = () => false) {
|
|
1133
|
-
const descriptors = Object.getOwnPropertyDescriptors(source)
|
|
1134
|
-
|
|
1135
|
-
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1136
|
-
if (skip(key)) {
|
|
1137
|
-
delete descriptors[key]
|
|
1138
|
-
continue
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
const descriptor = descriptors[key]
|
|
1142
|
-
// Accessor descriptors may hide state in closures or private fields.
|
|
1143
|
-
// Copying the accessor would not necessarily create an independent clone,
|
|
1144
|
-
// and reading it would execute user code. Custom objects that need this
|
|
1145
|
-
// should expose toClone() so they control how hidden state is copied.
|
|
1146
|
-
if (!Object.hasOwn(descriptor, 'value')) {
|
|
1147
|
-
cannotClone(source, String(key))
|
|
1148
|
-
}
|
|
1149
|
-
descriptor.value = cloneValue(descriptor.value, String(key))
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
Object.defineProperties(result, descriptors)
|
|
1153
|
-
return result
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function cloneArrayBuffer(value) {
|
|
1157
|
-
return value.slice(0)
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function cloneSharedArrayBuffer(value) {
|
|
1161
|
-
const result = new SharedArrayBuffer(value.byteLength)
|
|
1162
|
-
new Uint8Array(result).set(new Uint8Array(value))
|
|
1163
|
-
return result
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function cloneErrorObject(value, cloneValue, path) {
|
|
1167
|
-
const standardErrors = new Set([
|
|
1168
|
-
Error,
|
|
1169
|
-
EvalError,
|
|
1170
|
-
RangeError,
|
|
1171
|
-
ReferenceError,
|
|
1172
|
-
SyntaxError,
|
|
1173
|
-
TypeError,
|
|
1174
|
-
URIError,
|
|
1175
|
-
typeof AggregateError === 'undefined' ? undefined : AggregateError
|
|
1176
|
-
])
|
|
1177
|
-
|
|
1178
|
-
if (!standardErrors.has(value.constructor)) {
|
|
1179
|
-
cannotClone(value, path)
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const options = Object.hasOwn(value, 'cause')
|
|
1183
|
-
? { cause: cloneValue(value.cause, 'cause') }
|
|
1184
|
-
: undefined
|
|
1185
|
-
|
|
1186
|
-
if (typeof AggregateError !== 'undefined' && value instanceof AggregateError) {
|
|
1187
|
-
const errors = Array.from(value.errors || [], (error, index) => cloneValue(error, `errors.${index}`))
|
|
1188
|
-
return new AggregateError(errors, value.message, options)
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
return new value.constructor(value.message, options)
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
* Creates a non-reactive clone of a value or signal target.
|
|
1196
|
-
*
|
|
1197
|
-
* Deep-clones by default so nested signal data is not shared with the source. Built-in cloneable objects use their
|
|
1198
|
-
* native representation. Custom objects must provide toClone(); otherwise clone() throws instead of returning a
|
|
1199
|
-
* shared reference or copying only public properties.
|
|
1200
|
-
*
|
|
1201
|
-
* @param {*} value Value or signal target to clone.
|
|
1202
|
-
* @param {boolean|{deep?: boolean}} [options] false or { deep: false } keeps legacy shallow top-level cloning.
|
|
1203
|
-
* @returns {*} Non-reactive clone of value.
|
|
1204
|
-
* @throws {TypeError} If options are invalid, an unsupported object is encountered, an accessor property is found, or toClone() returns the original object.
|
|
1205
|
-
* @throws {*} Re-throws errors from custom toClone() methods.
|
|
1206
|
-
*/
|
|
1207
|
-
export function clone(value, options) {
|
|
1208
|
-
const { deep } = cloneOptions(options)
|
|
1209
|
-
const seen = new Map()
|
|
1210
|
-
|
|
1211
|
-
function cloneChild(value, path) {
|
|
1212
|
-
return deep ? cloneValue(value, path) : raw(value)
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function cloneValue(value, path = 'value') {
|
|
1216
|
-
const source = raw(value)
|
|
1217
|
-
|
|
1218
|
-
if (!isObjectLike(source)) {
|
|
1219
|
-
return source
|
|
1220
|
-
}
|
|
1221
|
-
if (seen.has(source)) {
|
|
1222
|
-
return seen.get(source)
|
|
1223
|
-
}
|
|
1224
|
-
if (hasToClone(source)) {
|
|
1225
|
-
const result = raw(source.toClone())
|
|
1226
|
-
if (Object.is(result, source)) {
|
|
1227
|
-
throw new TypeError(`simplyflow/state: clone() toClone() returned the original object at ${path}`)
|
|
1228
|
-
}
|
|
1229
|
-
seen.set(source, result)
|
|
1230
|
-
return result
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
if (Array.isArray(source)) {
|
|
1234
|
-
const result = new Array(source.length)
|
|
1235
|
-
seen.set(source, result)
|
|
1236
|
-
return cloneDescriptorProperties(source, result, cloneChild, key => key === 'length')
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (isPlainObject(source)) {
|
|
1240
|
-
const result = Object.create(Object.getPrototypeOf(source))
|
|
1241
|
-
seen.set(source, result)
|
|
1242
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (source instanceof Map) {
|
|
1246
|
-
const result = new Map()
|
|
1247
|
-
seen.set(source, result)
|
|
1248
|
-
source.forEach((mapValue, mapKey) => {
|
|
1249
|
-
result.set(cloneChild(mapKey, 'map key'), cloneChild(mapValue, 'map value'))
|
|
1250
|
-
})
|
|
1251
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
if (source instanceof Set) {
|
|
1255
|
-
const result = new Set()
|
|
1256
|
-
seen.set(source, result)
|
|
1257
|
-
source.forEach(setValue => result.add(cloneChild(setValue, 'set value')))
|
|
1258
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
if (source instanceof Date) {
|
|
1262
|
-
const result = new Date(source.getTime())
|
|
1263
|
-
seen.set(source, result)
|
|
1264
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (source instanceof RegExp) {
|
|
1268
|
-
const result = new RegExp(source.source, source.flags)
|
|
1269
|
-
result.lastIndex = source.lastIndex
|
|
1270
|
-
seen.set(source, result)
|
|
1271
|
-
return cloneDescriptorProperties(source, result, cloneChild, key => key === 'lastIndex')
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
if (source instanceof ArrayBuffer) {
|
|
1275
|
-
const result = cloneArrayBuffer(source)
|
|
1276
|
-
seen.set(source, result)
|
|
1277
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (typeof SharedArrayBuffer !== 'undefined' && source instanceof SharedArrayBuffer) {
|
|
1281
|
-
const result = cloneSharedArrayBuffer(source)
|
|
1282
|
-
seen.set(source, result)
|
|
1283
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
if (source instanceof DataView) {
|
|
1287
|
-
const buffer = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength)
|
|
1288
|
-
const result = new DataView(buffer)
|
|
1289
|
-
seen.set(source, result)
|
|
1290
|
-
return cloneDescriptorProperties(source, result, cloneChild)
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (isTypedArray(source)) {
|
|
1294
|
-
const result = new source.constructor(source)
|
|
1295
|
-
seen.set(source, result)
|
|
1296
|
-
return cloneDescriptorProperties(source, result, cloneChild, isIntegerKey)
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
if (typeof URL !== 'undefined' && source instanceof URL) {
|
|
1300
|
-
const result = new URL(source.href)
|
|
1301
|
-
seen.set(source, result)
|
|
1302
|
-
// Browser and jsdom URL objects can store implementation details in
|
|
1303
|
-
// own symbol properties. Copying those would couple the clone back
|
|
1304
|
-
// to the original, so the URL constructor is the complete clone.
|
|
1305
|
-
return result
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (typeof URLSearchParams !== 'undefined' && source instanceof URLSearchParams) {
|
|
1309
|
-
const result = new URLSearchParams(source)
|
|
1310
|
-
seen.set(source, result)
|
|
1311
|
-
return result
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
if (typeof File !== 'undefined' && source instanceof File) {
|
|
1315
|
-
const result = new File([source], source.name, {
|
|
1316
|
-
type: source.type,
|
|
1317
|
-
lastModified: source.lastModified
|
|
1318
|
-
})
|
|
1319
|
-
seen.set(source, result)
|
|
1320
|
-
return result
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if (typeof Blob !== 'undefined' && source instanceof Blob) {
|
|
1324
|
-
const result = source.slice(0, source.size, source.type)
|
|
1325
|
-
seen.set(source, result)
|
|
1326
|
-
return result
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
if (source instanceof Error) {
|
|
1330
|
-
const result = cloneErrorObject(source, cloneChild, path)
|
|
1331
|
-
seen.set(source, result)
|
|
1332
|
-
return cloneDescriptorProperties(source, result, cloneChild, key => (
|
|
1333
|
-
key === 'message' || key === 'cause' || key === 'errors' || key === 'stack'
|
|
1334
|
-
))
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (typeof Node !== 'undefined' && source instanceof Node && typeof source.cloneNode === 'function') {
|
|
1338
|
-
const result = source.cloneNode(deep)
|
|
1339
|
-
seen.set(source, result)
|
|
1340
|
-
return result
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
cannotClone(source, path)
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
return cloneValue(value)
|
|
1347
|
-
}
|
|
1
|
+
export * from '@muze-labs/simplyflow-state'
|