@zeix/cause-effect 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/system.ts ADDED
@@ -0,0 +1,122 @@
1
+ /* === Types === */
2
+
3
+ type Cleanup = () => void
4
+
5
+ type Watcher = {
6
+ (): void
7
+ unwatch(cleanup: Cleanup): void
8
+ cleanup(): void
9
+ }
10
+
11
+ /* === Internal === */
12
+
13
+ // Currently active watcher
14
+ let activeWatcher: Watcher | undefined
15
+
16
+ // Pending queue for batched change notifications
17
+ const pendingWatchers = new Set<Watcher>()
18
+ let batchDepth = 0
19
+
20
+ /* === Functions === */
21
+
22
+ /**
23
+ * Create a watcher that can be used to observe changes to a signal
24
+ *
25
+ * @since 0.14.1
26
+ * @param {() => void} watch - Function to be called when the state changes
27
+ * @returns {Watcher} - Watcher object with off and cleanup methods
28
+ */
29
+ const createWatcher = (watch: () => void): Watcher => {
30
+ const cleanups = new Set<Cleanup>()
31
+ const w = watch as Partial<Watcher>
32
+ w.unwatch = (cleanup: Cleanup) => {
33
+ cleanups.add(cleanup)
34
+ }
35
+ w.cleanup = () => {
36
+ for (const cleanup of cleanups) cleanup()
37
+ cleanups.clear()
38
+ }
39
+ return w as Watcher
40
+ }
41
+
42
+ /**
43
+ * Add active watcher to the Set of watchers
44
+ *
45
+ * @param {Set<Watcher>} watchers - watchers of the signal
46
+ */
47
+ const subscribe = (watchers: Set<Watcher>) => {
48
+ if (activeWatcher && !watchers.has(activeWatcher)) {
49
+ const watcher = activeWatcher
50
+ watcher.unwatch(() => {
51
+ watchers.delete(watcher)
52
+ })
53
+ watchers.add(watcher)
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Add watchers to the pending set of change notifications
59
+ *
60
+ * @param {Set<Watcher>} watchers - watchers of the signal
61
+ */
62
+ const notify = (watchers: Set<Watcher>) => {
63
+ for (const watcher of watchers) {
64
+ if (batchDepth) pendingWatchers.add(watcher)
65
+ else watcher()
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Flush all pending changes to notify watchers
71
+ */
72
+ const flush = () => {
73
+ while (pendingWatchers.size) {
74
+ const watchers = Array.from(pendingWatchers)
75
+ pendingWatchers.clear()
76
+ for (const watcher of watchers) watcher()
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Batch multiple changes in a single signal graph and DOM update cycle
82
+ *
83
+ * @param {() => void} fn - function with multiple signal writes to be batched
84
+ */
85
+ const batch = (fn: () => void) => {
86
+ batchDepth++
87
+ try {
88
+ fn()
89
+ } finally {
90
+ flush()
91
+ batchDepth--
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Run a function in a reactive context
97
+ *
98
+ * @param {() => void} run - function to run the computation or effect
99
+ * @param {Watcher} watcher - function to be called when the state changes or undefined for temporary unwatching while inserting auto-hydrating DOM nodes that might read signals (e.g., web components)
100
+ */
101
+ const observe = (run: () => void, watcher?: Watcher): void => {
102
+ const prev = activeWatcher
103
+ activeWatcher = watcher
104
+ try {
105
+ run()
106
+ } finally {
107
+ activeWatcher = prev
108
+ }
109
+ }
110
+
111
+ /* === Exports === */
112
+
113
+ export {
114
+ type Cleanup,
115
+ type Watcher,
116
+ subscribe,
117
+ notify,
118
+ flush,
119
+ batch,
120
+ createWatcher,
121
+ observe,
122
+ }
package/src/util.ts CHANGED
@@ -23,10 +23,6 @@ const isAsyncFunction = /*#__PURE__*/ <T>(
23
23
  ): fn is (...args: unknown[]) => Promise<T> =>
24
24
  isFunction(fn) && fn.constructor.name === 'AsyncFunction'
25
25
 
26
- const isDefinedObject = /*#__PURE__*/ (
27
- value: unknown,
28
- ): value is Record<string, unknown> => !!value && typeof value === 'object'
29
-
30
26
  const isObjectOfType = /*#__PURE__*/ <T>(
31
27
  value: unknown,
32
28
  type: string,
@@ -70,9 +66,7 @@ const toError = /*#__PURE__*/ (reason: unknown): Error =>
70
66
 
71
67
  const arrayToRecord = /*#__PURE__*/ <T>(array: T[]): Record<string, T> => {
72
68
  const record: Record<string, T> = {}
73
- for (let i = 0; i < array.length; i++) {
74
- record[String(i)] = array[i]
75
- }
69
+ for (let i = 0; i < array.length; i++) record[String(i)] = array[i]
76
70
  return record
77
71
  }
78
72
 
@@ -83,16 +77,14 @@ const recordToArray = /*#__PURE__*/ <T>(
83
77
  if (indexes === null) return record
84
78
 
85
79
  const array: T[] = []
86
- for (const index of indexes) {
87
- array.push(record[String(index)])
88
- }
80
+ for (const index of indexes) array.push(record[String(index)])
89
81
  return array
90
82
  }
91
83
 
92
84
  const valueString = /*#__PURE__*/ (value: unknown): string =>
93
85
  isString(value)
94
86
  ? `"${value}"`
95
- : isDefinedObject(value)
87
+ : !!value && typeof value === 'object'
96
88
  ? JSON.stringify(value)
97
89
  : String(value)
98
90
 
@@ -105,7 +97,6 @@ export {
105
97
  isSymbol,
106
98
  isFunction,
107
99
  isAsyncFunction,
108
- isDefinedObject,
109
100
  isObjectOfType,
110
101
  isRecord,
111
102
  isRecordOrArray,
@@ -1,14 +1,21 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { batch, computed, effect, match, resolve, state } from '../'
2
+ import {
3
+ batch,
4
+ createComputed,
5
+ createEffect,
6
+ createState,
7
+ match,
8
+ resolve,
9
+ } from '..'
3
10
 
4
11
  /* === Tests === */
5
12
 
6
13
  describe('Batch', () => {
7
14
  test('should be triggered only once after repeated state change', () => {
8
- const cause = state(0)
15
+ const cause = createState(0)
9
16
  let result = 0
10
17
  let count = 0
11
- effect((): undefined => {
18
+ createEffect((): undefined => {
12
19
  result = cause.get()
13
20
  count++
14
21
  })
@@ -22,13 +29,13 @@ describe('Batch', () => {
22
29
  })
23
30
 
24
31
  test('should be triggered only once when multiple signals are set', () => {
25
- const a = state(3)
26
- const b = state(4)
27
- const c = state(5)
28
- const sum = computed(() => a.get() + b.get() + c.get())
32
+ const a = createState(3)
33
+ const b = createState(4)
34
+ const c = createState(5)
35
+ const sum = createComputed(() => a.get() + b.get() + c.get())
29
36
  let result = 0
30
37
  let count = 0
31
- effect(() => {
38
+ createEffect(() => {
32
39
  const resolved = resolve({ sum })
33
40
  match(resolved, {
34
41
  ok: ({ sum: res }) => {
@@ -49,10 +56,10 @@ describe('Batch', () => {
49
56
 
50
57
  test('should prove example from README works', () => {
51
58
  // State: define an array of Signal<number>
52
- const signals = [state(2), state(3), state(5)]
59
+ const signals = [createState(2), createState(3), createState(5)]
53
60
 
54
61
  // Computed: derive a calculation ...
55
- const sum = computed(() => {
62
+ const sum = createComputed(() => {
56
63
  const v = signals.reduce((total, v) => total + v.get(), 0)
57
64
  if (!Number.isFinite(v)) throw new Error('Invalid value')
58
65
  return v
@@ -63,7 +70,7 @@ describe('Batch', () => {
63
70
  let errCount = 0
64
71
 
65
72
  // Effect: switch cases for the result
66
- effect(() => {
73
+ createEffect(() => {
67
74
  const resolved = resolve({ sum })
68
75
  match(resolved, {
69
76
  ok: ({ sum: v }) => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, mock, test } from 'bun:test'
2
- import { batch, computed, effect, state } from '../'
2
+ import { batch, createComputed, createEffect, createState } from '..'
3
3
  import { Counter, makeGraph, runGraph } from './util/dependency-graph'
4
4
  import type { Computed, ReactiveFramework } from './util/reactive-framework'
5
5
 
@@ -15,19 +15,19 @@ const busy = () => {
15
15
  const framework = {
16
16
  name: 'Cause & Effect',
17
17
  signal: <T extends {}>(initialValue: T) => {
18
- const s = state<T>(initialValue)
18
+ const s = createState<T>(initialValue)
19
19
  return {
20
20
  write: (v: T) => s.set(v),
21
21
  read: () => s.get(),
22
22
  }
23
23
  },
24
24
  computed: <T extends {}>(fn: () => T) => {
25
- const c = computed(fn)
25
+ const c = createComputed(fn)
26
26
  return {
27
27
  read: () => c.get(),
28
28
  }
29
29
  },
30
- effect: (fn: () => undefined) => effect(fn),
30
+ effect: (fn: () => undefined) => createEffect(fn),
31
31
  withBatch: (fn: () => undefined) => batch(fn),
32
32
  withBuild: <T>(fn: () => T) => fn(),
33
33
  }