@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.
- package/.ai-context.md +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +167 -159
- package/eslint.config.js +1 -1
- package/index.dev.js +528 -407
- package/index.js +1 -1
- package/index.ts +36 -25
- package/package.json +1 -1
- package/src/computed.ts +41 -30
- package/src/diff.ts +57 -44
- package/src/effect.ts +15 -16
- package/src/errors.ts +64 -0
- package/src/match.ts +2 -2
- package/src/resolve.ts +2 -2
- package/src/signal.ts +27 -49
- package/src/state.ts +27 -19
- package/src/store.ts +410 -209
- package/src/system.ts +122 -0
- package/src/util.ts +45 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +508 -72
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +61 -61
- package/test/match.test.ts +38 -28
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +19 -147
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +1370 -134
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +10 -9
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/diff.d.ts +5 -3
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +22 -0
- package/types/src/match.d.ts +1 -1
- package/types/src/resolve.d.ts +1 -1
- package/types/src/signal.d.ts +12 -19
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +40 -36
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +7 -5
- package/index.d.ts +0 -36
- package/src/scheduler.ts +0 -172
- 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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
106
|
+
isRecordOrArray,
|
|
70
107
|
hasMethod,
|
|
71
108
|
isAbortError,
|
|
72
109
|
toError,
|
|
73
|
-
|
|
110
|
+
arrayToRecord,
|
|
111
|
+
recordToArray,
|
|
112
|
+
valueString,
|
|
74
113
|
}
|
package/test/batch.test.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
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 =
|
|
15
|
+
const cause = createState(0)
|
|
9
16
|
let result = 0
|
|
10
17
|
let count = 0
|
|
11
|
-
|
|
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 =
|
|
26
|
-
const b =
|
|
27
|
-
const c =
|
|
28
|
-
const sum =
|
|
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
|
-
|
|
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 = [
|
|
59
|
+
const signals = [createState(2), createState(3), createState(5)]
|
|
53
60
|
|
|
54
61
|
// Computed: derive a calculation ...
|
|
55
|
-
const sum =
|
|
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
|
-
|
|
73
|
+
createEffect(() => {
|
|
67
74
|
const resolved = resolve({ sum })
|
|
68
75
|
match(resolved, {
|
|
69
76
|
ok: ({ sum: v }) => {
|
package/test/benchmark.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from 'bun:test'
|
|
2
|
-
import { batch,
|
|
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 =
|
|
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 =
|
|
25
|
+
const c = createComputed(fn)
|
|
26
26
|
return {
|
|
27
27
|
read: () => c.get(),
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
-
effect: (fn: () => undefined) =>
|
|
30
|
+
effect: (fn: () => undefined) => createEffect(fn),
|
|
31
31
|
withBatch: (fn: () => undefined) => batch(fn),
|
|
32
32
|
withBuild: <T>(fn: () => T) => fn(),
|
|
33
33
|
}
|