@zeix/cause-effect 0.15.1 → 0.16.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.
Files changed (48) hide show
  1. package/.ai-context.md +254 -0
  2. package/.cursorrules +54 -0
  3. package/.github/copilot-instructions.md +132 -0
  4. package/CLAUDE.md +319 -0
  5. package/README.md +167 -159
  6. package/eslint.config.js +1 -1
  7. package/index.dev.js +528 -407
  8. package/index.js +1 -1
  9. package/index.ts +36 -25
  10. package/package.json +1 -1
  11. package/src/computed.ts +41 -30
  12. package/src/diff.ts +57 -44
  13. package/src/effect.ts +15 -16
  14. package/src/errors.ts +64 -0
  15. package/src/match.ts +2 -2
  16. package/src/resolve.ts +2 -2
  17. package/src/signal.ts +27 -49
  18. package/src/state.ts +27 -19
  19. package/src/store.ts +410 -209
  20. package/src/system.ts +122 -0
  21. package/src/util.ts +45 -6
  22. package/test/batch.test.ts +18 -11
  23. package/test/benchmark.test.ts +4 -4
  24. package/test/computed.test.ts +508 -72
  25. package/test/diff.test.ts +321 -4
  26. package/test/effect.test.ts +61 -61
  27. package/test/match.test.ts +38 -28
  28. package/test/resolve.test.ts +16 -16
  29. package/test/signal.test.ts +19 -147
  30. package/test/state.test.ts +212 -25
  31. package/test/store.test.ts +1370 -134
  32. package/test/util/dependency-graph.ts +1 -1
  33. package/types/index.d.ts +10 -9
  34. package/types/src/collection.d.ts +26 -0
  35. package/types/src/computed.d.ts +9 -9
  36. package/types/src/diff.d.ts +5 -3
  37. package/types/src/effect.d.ts +3 -3
  38. package/types/src/errors.d.ts +22 -0
  39. package/types/src/match.d.ts +1 -1
  40. package/types/src/resolve.d.ts +1 -1
  41. package/types/src/signal.d.ts +12 -19
  42. package/types/src/state.d.ts +5 -5
  43. package/types/src/store.d.ts +40 -36
  44. package/types/src/system.d.ts +44 -0
  45. package/types/src/util.d.ts +7 -5
  46. package/index.d.ts +0 -36
  47. package/src/scheduler.ts +0 -172
  48. package/types/test-new-effect.d.ts +0 -1
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
@@ -1,3 +1,8 @@
1
+ /* === Constants === */
2
+
3
+ // biome-ignore lint/suspicious/noExplicitAny: Deliberately using any to be used as a placeholder value in any signal
4
+ const UNSET: any = Symbol()
5
+
1
6
  /* === Utility Functions === */
2
7
 
3
8
  const isString = /*#__PURE__*/ (value: unknown): value is string =>
@@ -6,6 +11,9 @@ const isString = /*#__PURE__*/ (value: unknown): value is string =>
6
11
  const isNumber = /*#__PURE__*/ (value: unknown): value is number =>
7
12
  typeof value === 'number'
8
13
 
14
+ const isSymbol = /*#__PURE__*/ (value: unknown): value is symbol =>
15
+ typeof value === 'symbol'
16
+
9
17
  const isFunction = /*#__PURE__*/ <T>(
10
18
  fn: unknown,
11
19
  ): fn is (...args: unknown[]) => T => typeof fn === 'function'
@@ -24,6 +32,12 @@ const isRecord = /*#__PURE__*/ <T extends Record<string, unknown>>(
24
32
  value: unknown,
25
33
  ): value is T => isObjectOfType(value, 'Object')
26
34
 
35
+ const isRecordOrArray = /*#__PURE__*/ <
36
+ T extends Record<string | number, unknown> | ReadonlyArray<unknown>,
37
+ >(
38
+ value: unknown,
39
+ ): value is T => isRecord(value) || Array.isArray(value)
40
+
27
41
  const validArrayIndexes = /*#__PURE__*/ (
28
42
  keys: Array<PropertyKey>,
29
43
  ): number[] | null => {
@@ -50,25 +64,50 @@ const isAbortError = /*#__PURE__*/ (error: unknown): boolean =>
50
64
  const toError = /*#__PURE__*/ (reason: unknown): Error =>
51
65
  reason instanceof Error ? reason : Error(String(reason))
52
66
 
53
- class CircularDependencyError extends Error {
54
- constructor(where: string) {
55
- super(`Circular dependency in ${where} detected`)
56
- this.name = 'CircularDependencyError'
67
+ const arrayToRecord = /*#__PURE__*/ <T>(array: T[]): Record<string, T> => {
68
+ const record: Record<string, T> = {}
69
+ for (let i = 0; i < array.length; i++) {
70
+ record[String(i)] = array[i]
57
71
  }
72
+ return record
58
73
  }
59
74
 
75
+ const recordToArray = /*#__PURE__*/ <T>(
76
+ record: Record<string | number, T>,
77
+ ): Record<string, T> | T[] => {
78
+ const indexes = validArrayIndexes(Object.keys(record))
79
+ if (indexes === null) return record
80
+
81
+ const array: T[] = []
82
+ for (const index of indexes) {
83
+ array.push(record[String(index)])
84
+ }
85
+ return array
86
+ }
87
+
88
+ const valueString = /*#__PURE__*/ (value: unknown): string =>
89
+ isString(value)
90
+ ? `"${value}"`
91
+ : !!value && typeof value === 'object'
92
+ ? JSON.stringify(value)
93
+ : String(value)
94
+
60
95
  /* === Exports === */
61
96
 
62
97
  export {
98
+ UNSET,
63
99
  isString,
64
100
  isNumber,
101
+ isSymbol,
65
102
  isFunction,
66
103
  isAsyncFunction,
67
104
  isObjectOfType,
68
105
  isRecord,
69
- validArrayIndexes,
106
+ isRecordOrArray,
70
107
  hasMethod,
71
108
  isAbortError,
72
109
  toError,
73
- CircularDependencyError,
110
+ arrayToRecord,
111
+ recordToArray,
112
+ valueString,
74
113
  }
@@ -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
  }