simplyflow 0.8.2 → 0.9.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/src/state.mjs CHANGED
@@ -1,55 +1,90 @@
1
- if (!Symbol.iterate) {
2
- Symbol.iterate = Symbol('iterate')
1
+ import { DEP } from './symbols.mjs'
2
+
3
+ function wrapMapMethod(target, property, receiver, value) {
4
+ return (...args) => {
5
+ if (property === 'get' || property === 'has') {
6
+ notifyGet(receiver, args[0])
7
+ }
8
+ if (['keys', 'values', 'entries', 'forEach', Symbol.iterator].includes(property)) {
9
+ notifyGet(receiver, DEP.ITERATE)
10
+ }
11
+
12
+ const oldSize = target.size
13
+ const result = value.apply(target, args)
14
+
15
+ if (property === 'set') {
16
+ notifySet(receiver, makeContext(args[0], { now: args[1] }))
17
+ }
18
+
19
+ if (property === 'delete') {
20
+ notifySet(receiver, makeContext(args[0], { delete: true}))
21
+ }
22
+
23
+ if (oldSize !== target.size) {
24
+ notifySet(receiver, makeContext(DEP.SIZE, {}))
25
+ }
26
+
27
+ if (['set','delete','clear'].includes(property) || oldSize!==target.size) {
28
+ notifySet(receiver, makeContext(DEP.ITERATE, {}))
29
+ }
30
+
31
+ return result
32
+ }
33
+
3
34
  }
4
- if (!Symbol.xRay) {
5
- Symbol.xRay = Symbol('xRay')
35
+
36
+ function wrapArrayMethod(target, property, receiver, value) {
37
+ return (...args) => {
38
+ let l = target.length
39
+ // by binding the function to the receiver
40
+ // all accesses in the function will be trapped
41
+ // by the Proxy, so get/set/delete is all handled
42
+ let result = value.apply(receiver, args)
43
+ if (l != target.length) {
44
+ notifySet(receiver, makeContext(DEP.LENGTH, { was: l, now: target.length }) )
45
+ }
46
+ return result
47
+ }
6
48
  }
7
- if (!Symbol.Signal) {
8
- Symbol.Signal = Symbol('Signal')
49
+
50
+ function wrapSetMethod(target, property, receiver, value) {
51
+ return (...args) => {
52
+ // node doesn't allow you to call set/map functions
53
+ // bound to the receiver.. so using target instead
54
+ // there are no properties to update anyway, except for size
55
+ let s = target.size
56
+ let result = value.apply(target, args)
57
+ if (s != target.size) {
58
+ notifySet(receiver, makeContext( DEP.SIZE, { was: s, now: target.size }) )
59
+ }
60
+ // there is no efficient way to see if the function called
61
+ // has actually changed the Set/Map, but by assuming the
62
+ // 'setter' functions will change the results of the
63
+ // 'getter' functions, effects should update correctly
64
+ if (['set','add','clear','delete'].includes(property)) {
65
+ notifySet(receiver, makeContext( { entries: {}, forEach: {}, has: {}, keys: {}, values: {}, [Symbol.iterator]: {} } ) )
66
+ }
67
+ return result
68
+ }
9
69
  }
10
70
 
11
71
  const signalHandler = {
12
72
  get: (target, property, receiver) => {
13
- if (property===Symbol.xRay) {
73
+ if (property===DEP.XRAY) {
14
74
  return target // don't notifyGet here, this is only called by set
15
75
  }
16
- if (property===Symbol.Signal) {
76
+ if (property===DEP.SIGNAL) {
17
77
  return true
18
78
  }
19
79
  const value = target?.[property] // Reflect.get fails on a Set.
20
80
  notifyGet(receiver, property)
21
81
  if (typeof value === 'function') {
22
82
  if (Array.isArray(target)) {
23
- return (...args) => {
24
- let l = target.length
25
- // by binding the function to the receiver
26
- // all accesses in the function will be trapped
27
- // by the Proxy, so get/set/delete is all handled
28
- let result = value.apply(receiver, args)
29
- if (l != target.length) {
30
- notifySet(receiver, makeContext('length', { was: l, now: target.length }) )
31
- }
32
- return result
33
- }
34
- } else if (target instanceof Set || target instanceof Map) {
35
- return (...args) => {
36
- // node doesn't allow you to call set/map functions
37
- // bound to the receiver.. so using target instead
38
- // there are no properties to update anyway, except for size
39
- let s = target.size
40
- let result = value.apply(target, args)
41
- if (s != target.size) {
42
- notifySet(receiver, makeContext( 'size', { was: s, now: target.size }) )
43
- }
44
- // there is no efficient way to see if the function called
45
- // has actually changed the Set/Map, but by assuming the
46
- // 'setter' functions will change the results of the
47
- // 'getter' functions, effects should update correctly
48
- if (['set','add','clear','delete'].includes(property)) {
49
- notifySet(receiver, makeContext( { entries: {}, forEach: {}, has: {}, keys: {}, values: {}, [Symbol.iterator]: {} } ) )
50
- }
51
- return result
52
- }
83
+ return wrapArrayMethod(target, property, receiver, value)
84
+ } else if (target instanceof Map) {
85
+ return wrapMapMethod(target, property, receiver, value)
86
+ } else if (target instanceof Set) {
87
+ return wrapSetMethod(target, property, receiver, value)
53
88
  } else if (
54
89
  target instanceof HTMLElement
55
90
  || target instanceof Number
@@ -74,8 +109,8 @@ const signalHandler = {
74
109
  notifySet(receiver, makeContext(property, { was: current, now: value } ) )
75
110
  }
76
111
  if (typeof current === 'undefined') {
77
- notifySet(receiver, makeContext(Symbol.iterate, {}))
78
- notifySet(receiver, makeContext('length', {}))
112
+ notifySet(receiver, makeContext(DEP.ITERATE, {}))
113
+ notifySet(receiver, makeContext(DEP.LENGTH, {}))
79
114
  }
80
115
  return true
81
116
  },
@@ -92,19 +127,21 @@ const signalHandler = {
92
127
  delete target[property]
93
128
  let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
94
129
  notifySet(receiver, makeContext(property,{ delete: true, was: current }))
130
+ notifySet(receiver, makeContext(DEP.ITERATE, { delete: true, property })
131
+ )
95
132
  }
96
133
  return true
97
134
  },
98
135
  defineProperty: (target, property, descriptor) => {
99
136
  if (typeof target[property] === 'undefined') {
100
137
  let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
101
- notifySet(receiver, makeContext(Symbol.iterate, {}))
138
+ notifySet(receiver, makeContext(DEP.ITERATE, {}))
102
139
  }
103
140
  return Object.defineProperty(target, property, descriptor)
104
141
  },
105
142
  ownKeys: (target) => {
106
143
  let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
107
- notifyGet(receiver, Symbol.iterate)
144
+ notifyGet(receiver, DEP.ITERATE)
108
145
  return Reflect.ownKeys(target)
109
146
  }
110
147
 
@@ -122,11 +159,13 @@ export const signals = new WeakMap()
122
159
  * Creates a new signal proxy of the given object, that intercepts get/has and set/delete
123
160
  * to allow reactive functions to be triggered when signal values change.
124
161
  */
125
- export function signal(v) {
126
- if (!v) {
127
- v = {}
162
+ export function signal(v = {}) {
163
+ if (v === null || typeof v !== 'object' && typeof v !== 'function') {
164
+ throw new TypeError(
165
+ `simplyflow/state: signal() expects an object, array, Map, Set, class instance, or function; received ${typeof v}`
166
+ )
128
167
  }
129
- if (v[Symbol.Signal]) { // there can be only one signal for any value
168
+ if (v[DEP.SIGNAL]) { // there can be only one signal for any value
130
169
  return v
131
170
  }
132
171
  if (!signals.has(v)) {
@@ -153,21 +192,30 @@ let tracing = false
153
192
  * call the given function and then disable all tracers.
154
193
  * @return void
155
194
  */
156
- export function trace(signal, prop) {
157
- if (typeof signal==='function') {
195
+ export function trace(target, prop) {
196
+ if (typeof target==='function') {
158
197
  tracing = true
159
- signal()
160
- tracing = false
161
- } else {
162
- const listeners = getListeners(signal, prop)
163
- return listeners.map(listener => {
164
- return {
165
- effect: listener.effectType,
166
- fn: listener.effectFunction,
167
- signal: signals.get(listener.effectFunction)
168
- }
169
- })
198
+ try {
199
+ return target()
200
+ } finally {
201
+ tracing = false
202
+ }
203
+ }
204
+
205
+ if (!target || !target[DEP.SIGNAL]) {
206
+ throw new TypeError(
207
+ 'simplyflow/state: trace() expects either a function or a signal'
208
+ )
170
209
  }
210
+
211
+ const listeners = getListeners(target, prop)
212
+ return listeners.map(listener => {
213
+ return {
214
+ effect: listener.effectType,
215
+ fn: listener.effectFunction,
216
+ signal: signals.get(listener.effectFunction)
217
+ }
218
+ })
171
219
  }
172
220
 
173
221
  /**
@@ -180,6 +228,9 @@ export function trace(signal, prop) {
180
228
  * set: function(signal, context, listener)
181
229
  */
182
230
  export function addTracer(tracer) {
231
+ if (!tracer || typeof tracer !== 'object') {
232
+ throw new TypeError('simplyflow/state: addTracer() expects a tracer object')
233
+ }
183
234
  if (!tracer.get && !tracer.set) {
184
235
  throw new Error('simply.state: addTracer: missing "get" or "set" property in tracer', tracer)
185
236
  }
@@ -207,7 +258,14 @@ let batchMode = 0
207
258
  * Triggers any reactor function that depends on this signal
208
259
  * to re-compute its values
209
260
  */
210
- export function notifySet(self, context={}) {
261
+ export function notifySet(self, context = new Map()) {
262
+ if (!self || !self[DEP.SIGNAL]) {
263
+ throw new TypeError('simplyflow/state: notifySet() expects a signal as first argument')
264
+ }
265
+ if (!(context instanceof Map)) {
266
+ throw new TypeError('simplyflow/state: notifySet() expects context to be a Map; use makeContext()')
267
+ }
268
+
211
269
  let listeners = []
212
270
  context.forEach((change, property) => {
213
271
  let propListeners = getListeners(self, property)
@@ -240,8 +298,14 @@ export function notifySet(self, context={}) {
240
298
 
241
299
  export function makeContext(property, change) {
242
300
  let context = new Map()
243
- if (typeof property === 'object') {
244
- for (let prop in property) {
301
+ if (property instanceof Map) {
302
+ property.forEach((change, prop) => {
303
+ context.set(prop, change)
304
+ })
305
+ return context
306
+ }
307
+ if (property !== null && typeof property === 'object') {
308
+ for (const prop of Reflect.ownKeys(property)) {
245
309
  context.set(prop, property[prop])
246
310
  }
247
311
  } else {
@@ -333,17 +397,22 @@ function setListeners(self, property, compute) {
333
397
  * based on the current call (code path)
334
398
  */
335
399
  function clearListeners(compute) {
336
- let connectedSignals = computeMap.get(compute)
337
- if (connectedSignals) {
338
- connectedSignals.forEach(property => {
339
- property.forEach(s => {
340
- let listeners = listenersMap.get(s)
341
- if (listeners.has(property)) {
342
- listeners.get(property).delete(compute)
343
- }
344
- })
345
- })
400
+ const connectedSignals = computeMap.get(compute)
401
+ if (!connectedSignals) {
402
+ return
346
403
  }
404
+
405
+ connectedSignals.forEach((signals, property) => {
406
+ signals.forEach(signal => {
407
+ const listeners = listenersMap.get(signal)
408
+
409
+ if (listeners?.has(property)) {
410
+ listeners.get(property).delete(compute)
411
+ }
412
+ })
413
+ })
414
+
415
+ computeMap.delete(compute)
347
416
  }
348
417
 
349
418
  /**
@@ -367,11 +436,17 @@ const effectMap = new WeakMap()
367
436
  */
368
437
  const signalStack = []
369
438
 
439
+ function assertFunction(fn, name) {
440
+ if (typeof fn !== 'function') {
441
+ throw new TypeError(`simplyflow/state: ${name}() expects a function`)
442
+ }
443
+ }
370
444
  /**
371
445
  * Runs the given function at once, and then whenever a signal changes that
372
446
  * is used by the given function (or at least signals used in the previous run).
373
447
  */
374
448
  export function effect(fn) {
449
+ assertFunction(fn, 'effect')
375
450
  if (effectStack.findIndex(f => fn==f)!==-1) {
376
451
  throw new Error('Recursive update() call', {cause:fn})
377
452
  }
@@ -427,8 +502,11 @@ export function effect(fn) {
427
502
 
428
503
 
429
504
  export function destroy(connectedSignal) {
505
+ if (!connectedSignal || !connectedSignal[DEP.SIGNAL]) {
506
+ throw new TypeError('simplyflow/state: destroy() expects an effect signal')
507
+ }
430
508
  // find the computeEffect associated with this signal
431
- const computeEffect = effectMap.get(connectedSignal)?.deref()
509
+ const computeEffect = effectMap.get(connectedSignal)
432
510
  if (!computeEffect) {
433
511
  return
434
512
  }
@@ -437,11 +515,11 @@ export function destroy(connectedSignal) {
437
515
  clearListeners(computeEffect)
438
516
 
439
517
  // remove all references to connectedSignal
440
- let fn = computeEffect.fn
441
- signals.remove(fn)
518
+ if (computeEffect.fn) {
519
+ signals.delete(computeEffect.fn)
520
+ }
442
521
 
443
522
  effectMap.delete(connectedSignal)
444
-
445
523
  // if no other references to connectedSignal exist, it will be garbage collected
446
524
  }
447
525
 
@@ -454,6 +532,7 @@ export function destroy(connectedSignal) {
454
532
  * @result mixed the result of the fn() function call
455
533
  */
456
534
  export function batch(fn) {
535
+ assertFunction(fn, 'batch')
457
536
  batchMode++
458
537
  let result
459
538
  try {
@@ -496,6 +575,12 @@ function runBatchedListeners() {
496
575
  * @returns signal with the result of the effect function fn
497
576
  */
498
577
  export function throttledEffect(fn, throttleTime) {
578
+ assertFunction(fn, 'throttledEffect')
579
+ if (!Number.isFinite(throttleTime) || throttleTime < 0) {
580
+ throw new TypeError(
581
+ `simplyflow/state: throttledEffect() expects throttleTime to be a non-negative number`
582
+ )
583
+ }
499
584
  if (effectStack.findIndex(f => fn==f)!==-1) {
500
585
  throw new Error('Recursive update() call', {cause:fn})
501
586
  }
@@ -509,18 +594,41 @@ export function throttledEffect(fn, throttleTime) {
509
594
  signals.set(fn, connectedSignal)
510
595
  }
511
596
 
512
- let throttled = false
597
+ let throttledUntil = 0
513
598
  let hasChange = true
599
+ let timeout = null
600
+
601
+ function schedule() {
602
+ if (timeout) {
603
+ return
604
+ }
605
+
606
+ const delay = Math.max(0, throttledUntil - Date.now())
607
+
608
+ timeout = globalThis.setTimeout(() => {
609
+ timeout = null
610
+
611
+ if (hasChange) {
612
+ computeEffect()
613
+ }
614
+ }, delay)
615
+ }
616
+
514
617
  // this is the function that is called automatically
515
618
  // whenever a signal dependency changes
516
619
  const computeEffect = function computeEffect() {
517
- if (signalStack.findIndex(s => s==connectedSignal)!==-1) {
518
- throw new Error('Cyclical dependency in update() call', { cause: fn})
519
- }
520
- if (throttled && throttled>Date.now()) {
620
+ const now = Date.now()
621
+
622
+ if (throttledUntil > now) {
521
623
  hasChange = true
624
+ schedule()
522
625
  return
523
626
  }
627
+
628
+ if (signalStack.findIndex(s => s==connectedSignal)!==-1) {
629
+ throw new Error('Cyclical dependency in update() call', { cause: fn})
630
+ }
631
+
524
632
  // remove all dependencies (signals) from previous runs
525
633
  clearListeners(computeEffect)
526
634
  // record new dependencies on this run
@@ -547,12 +655,8 @@ export function throttledEffect(fn, throttleTime) {
547
655
  connectedSignal.current = result
548
656
  }
549
657
  }
550
- throttled = Date.now()+throttleTime
551
- globalThis.setTimeout(() => {
552
- if (hasChange) {
553
- computeEffect()
554
- }
555
- }, throttleTime)
658
+ throttledUntil = Date.now()+throttleTime
659
+ schedule()
556
660
  }
557
661
  // run the computEffect immediately upon creation
558
662
  computeEffect()
@@ -565,6 +669,12 @@ export function throttledEffect(fn, throttleTime) {
565
669
  // on clock.tick() (or clock.time++) run only the clock.needsUpdate effects
566
670
  // (first create a copy and reset clock.needsUpdate, then run effects)
567
671
  export function clockEffect(fn, clock) {
672
+ assertFunction(fn, 'clockEffect')
673
+ if (!clock || typeof clock !== 'object' || typeof clock.time !== 'number') {
674
+ throw new TypeError(
675
+ `simplyflow/state: clockEffect() expects a clock object with a numeric .time property`
676
+ )
677
+ }
568
678
  let connectedSignal = signals.get(fn)
569
679
  if (!connectedSignal) {
570
680
  connectedSignal = signal({
@@ -623,6 +733,7 @@ export function clockEffect(fn, clock) {
623
733
  * @result mixed
624
734
  */
625
735
  export function untracked(fn) {
736
+ assertFunction(fn, 'untracked')
626
737
  const pos = computeStack.length-1
627
738
  const remember = computeStack[pos]
628
739
  computeStack[pos] = false
@@ -635,7 +746,6 @@ export function untracked(fn) {
635
746
 
636
747
  /**
637
748
  * This function will created a copy of the input, recursive if deep==true
638
- * It will replace any non-clonable values with null
639
749
  * It will replace signals with a clone of their target
640
750
  * It will keep cyclical references intact
641
751
  * @param value Object
@@ -655,16 +765,16 @@ export function clone(value, deep=false)
655
765
  return value
656
766
  }
657
767
  if (Array.isArray(value)) {
658
- let result = []
768
+ const result = []
659
769
  if (!deep) {
660
- result = value.slice()
661
- seen.set(value, result)
662
- return result
770
+ return value.slice()
663
771
  }
772
+
664
773
  seen.set(value, result)
665
- for (const key of value) {
666
- result[key] = innerClone(value[key])
774
+ for (let i=0; i<value.length; i++) {
775
+ result[i] = innerClone(value[i])
667
776
  }
777
+ return result
668
778
  } else if (!value.constructor || value.constructor===Object) {
669
779
  let result = {}
670
780
  if (!value.constructor) {
@@ -681,7 +791,7 @@ export function clone(value, deep=false)
681
791
  }
682
792
  break
683
793
  default:
684
- return null // ignore non-cloneable values
794
+ return value // primitive
685
795
  break
686
796
  }
687
797
  }
@@ -0,0 +1,8 @@
1
+ export const DEP = {
2
+ ITERATE: Symbol.for('@simplyedit/simplyflow.iterate'),
3
+ XRAY: Symbol.for('@simplyedit/simplyflow.xRay'),
4
+ SIGNAL: Symbol.for('@simplyedit/simplyflow.Signal'),
5
+ TEMPLATE: Symbol.for('@simplyedit/simplyflow.bindTemplate'),
6
+ LENGTH: 'length',
7
+ SIZE: 'size'
8
+ }