@cplieger/reactive 1.0.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/.editorconfig +19 -0
- package/.htmlvalidate.json +28 -0
- package/.stylelintrc.json +67 -0
- package/LICENSE +692 -0
- package/README.md +120 -0
- package/cliff.toml +37 -0
- package/eslint.config.base.mjs +187 -0
- package/jsr.json +22 -0
- package/package.json +35 -0
- package/src/index.ts +25 -0
- package/src/reconcile-tree.ts +169 -0
- package/src/reconcile.ts +55 -0
- package/src/signal.ts +687 -0
- package/src/store.ts +159 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Typed per-key reactive store with auto-tracked effects, computed, and batching.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// import { createStore } from './store.js';
|
|
5
|
+
// interface MyMap { count: number; name: string }
|
|
6
|
+
// const { get, set, subscribe, effect, computed, batch } = createStore<MyMap>();
|
|
7
|
+
|
|
8
|
+
type Callback = (value: unknown) => void;
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- void is intentional for effect return
|
|
10
|
+
type Cleanup = void | (() => void);
|
|
11
|
+
|
|
12
|
+
/** A typed per-key reactive store with auto-tracked effects, computed, and batching. */
|
|
13
|
+
export interface Store<M> {
|
|
14
|
+
get<K extends keyof M & string>(key: K): M[K];
|
|
15
|
+
set<K extends keyof M & string>(key: K, value: M[K]): void;
|
|
16
|
+
subscribe<K extends keyof M & string>(key: K, cb: (value: M[K]) => void): () => void;
|
|
17
|
+
effect(fn: () => Cleanup): () => void;
|
|
18
|
+
computed<K extends keyof M & string>(outputKey: K, fn: () => M[K]): () => void;
|
|
19
|
+
batch(fn: () => void): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Create a typed reactive store. Keys are auto-tracked in effects; writes notify subscribers. */
|
|
23
|
+
export function createStore<M>(): Store<M> {
|
|
24
|
+
const state: Record<string, unknown> = {};
|
|
25
|
+
const subscribers: Record<string, Callback[]> = {};
|
|
26
|
+
|
|
27
|
+
let tracking: Set<string> | null = null;
|
|
28
|
+
let batchDepth = 0;
|
|
29
|
+
let pendingKeys: Map<string, unknown> | null = null;
|
|
30
|
+
|
|
31
|
+
function notifySubs(key: string, value: unknown): void {
|
|
32
|
+
const cbs = subscribers[key];
|
|
33
|
+
if (!cbs) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const cb of cbs) {
|
|
37
|
+
try {
|
|
38
|
+
cb(value);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("store subscriber error:", e);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function get<K extends keyof M & string>(key: K): M[K] {
|
|
46
|
+
if (tracking) {
|
|
47
|
+
tracking.add(key);
|
|
48
|
+
}
|
|
49
|
+
return state[key] as M[K];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function set<K extends keyof M & string>(key: K, value: M[K]): void {
|
|
53
|
+
const prev = state[key];
|
|
54
|
+
state[key] = value;
|
|
55
|
+
if (Object.is(prev, value)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (batchDepth > 0) {
|
|
59
|
+
pendingKeys ??= new Map();
|
|
60
|
+
pendingKeys.set(key, value);
|
|
61
|
+
} else {
|
|
62
|
+
notifySubs(key, value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function subscribe<K extends keyof M & string>(key: K, cb: (value: M[K]) => void): () => void {
|
|
67
|
+
subscribers[key] ??= [];
|
|
68
|
+
subscribers[key].push(cb as Callback);
|
|
69
|
+
return () => {
|
|
70
|
+
const arr = subscribers[key];
|
|
71
|
+
if (arr) {
|
|
72
|
+
subscribers[key] = arr.filter((c) => c !== cb);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function storeBatch(fn: () => void): void {
|
|
78
|
+
batchDepth++;
|
|
79
|
+
try {
|
|
80
|
+
fn();
|
|
81
|
+
} finally {
|
|
82
|
+
batchDepth--;
|
|
83
|
+
if (batchDepth === 0 && pendingKeys) {
|
|
84
|
+
const p = pendingKeys;
|
|
85
|
+
pendingKeys = null;
|
|
86
|
+
for (const [k, v] of p) {
|
|
87
|
+
notifySubs(k, v);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function storeEffect(fn: () => Cleanup): () => void {
|
|
94
|
+
let unsubs: (() => void)[] = [];
|
|
95
|
+
let cleanup: Cleanup;
|
|
96
|
+
let disposed = false;
|
|
97
|
+
|
|
98
|
+
const run = (): void => {
|
|
99
|
+
if (disposed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const u of unsubs) {
|
|
103
|
+
u();
|
|
104
|
+
}
|
|
105
|
+
unsubs = [];
|
|
106
|
+
if (cleanup) {
|
|
107
|
+
const c = cleanup;
|
|
108
|
+
cleanup = undefined;
|
|
109
|
+
try {
|
|
110
|
+
c();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error("store effect cleanup error:", e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const prev = tracking;
|
|
116
|
+
tracking = new Set();
|
|
117
|
+
try {
|
|
118
|
+
cleanup = fn();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error("store effect error:", e);
|
|
121
|
+
}
|
|
122
|
+
const deps = tracking;
|
|
123
|
+
tracking = prev;
|
|
124
|
+
for (const dep of deps) {
|
|
125
|
+
unsubs.push(
|
|
126
|
+
subscribe(dep as keyof M & string, run as (value: M[keyof M & string]) => void),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
run();
|
|
131
|
+
return () => {
|
|
132
|
+
if (disposed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
disposed = true;
|
|
136
|
+
for (const u of unsubs) {
|
|
137
|
+
u();
|
|
138
|
+
}
|
|
139
|
+
unsubs = [];
|
|
140
|
+
if (cleanup) {
|
|
141
|
+
const c = cleanup;
|
|
142
|
+
cleanup = undefined;
|
|
143
|
+
try {
|
|
144
|
+
c();
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error("store effect cleanup error:", e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function computed<K extends keyof M & string>(outputKey: K, fn: () => M[K]): () => void {
|
|
153
|
+
return storeEffect(() => {
|
|
154
|
+
set(outputKey, fn());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { get, set, subscribe, effect: storeEffect, computed, batch: storeBatch };
|
|
159
|
+
}
|