-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstore.js
More file actions
201 lines (173 loc) · 7.48 KB
/
store.js
File metadata and controls
201 lines (173 loc) · 7.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//supports "import", global "<script>" and pasting in your JS.
(function (root, factory) {
const api = factory();
// Support global "StoreLib" for paste or use via <script>
const ns = root.StoreLib || (root.StoreLib = {});
for (const k in api) ns[k] = api[k];
})(typeof globalThis !== 'undefined' ? globalThis
: typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: this, function () {
/* Reactive store
* - get() snapshot
* - set(value) replace
* - patch(value) shallow merge
* - subscribe(callback, selector) -> unsubscribe
*
* Mutations batch notifications on next paint via requestAnimationFrame.
* (queueMicrotask or setTimeout are used as fallbacks for non-visual environments.)
*
* Subscribers receive updates only when their selected value actually changes,
* using shallow comparison for plain objects and arrays, and Object.is for primitives.
*
* For safety, any failure in a callback or selector automatically unsubscribes that subscriber.
*/
/**
* Create a reactive store around the provided initial state.
* @template T
* @param {T} initialState Initial value for the store. Mutations replace or
* merge against this value; consumers should treat it as immutable.
* @returns {{get: () => T, set: (newState: T) => void, patch: (partial: Partial<T>) => void, subscribe: (callback: (value: any, isInit: boolean) => void, selector?: (state: T) => any) => () => void}}
* An object exposing snapshot access, mutation helpers, and reactive subscription.
*/
function createStore(initialState) {
var state = initialState;
var subscribers = new Set(); // { callback, selector, prev, active }
var scheduled = false;
var scheduleFn = pickScheduler();
/**
* Obtain the current snapshot of the store.
* @returns {T} The latest state value. Callers must not mutate the
* returned object directly; use the provided mutation helpers instead.
*/
function get() { return state; }
/**
* Replace the entire state and queue notifications to subscribers.
* @param {T} newState The new value to use for the store.
* @returns {void}
*/
function set(newState) {
state = newState;
scheduleNotify();
}
/**
* Perform a shallow merge with the current state and queue notifications to subscribers.
* @param {Partial<T>} partial Object containing properties to merge into a shallow clone of the current state.
* @returns {void}
*/
function patch(partial) {
set(Object.assign({}, state, partial));
}
/**
* Subscribe to state changes with an optional selector function.
* @param {(value: any, isInit: boolean) => void} callback Invoked with
* the selected value. Receives `isInit = true` for the synchronous
* initial call, then `false` for subsequent updates.
* @param {(state: T) => any} [selector] A selector that derives the
* relevant portion of the state. Defaults to the identity function.
* The callback only fires when the selected value changes (shallow
* comparison for arrays/objects, `Object.is` otherwise).
* @returns {() => void} An unsubscribe function. Once invoked the
* subscription is removed; callbacks throwing an error also cause
* automatic unsubscription.
*/
function subscribe(callback, selector) {
if (!selector) selector = (x) => x;
let prev;
try {
prev = selector(state);
} catch (err) {
console.error(err);
throw err;
}
const sub = { callback: callback, selector: selector, prev: prev, active: true };
subscribers.add(sub);
try {
callback(prev, true); // firstTime = true
} catch (err) {
console.error(err);
unsubscribe(sub);
throw err;
}
return () => { if (sub.active) unsubscribe(sub); };
}
// ---- internal helpers ----
// explicitly remove subscriber
function unsubscribe(sub) {
sub.active = false;
subscribers.delete(sub);
}
function scheduleNotify() {
if (scheduled) return;
scheduled = true;
scheduleFn(runNotifications);
}
/* choose scheduler depending on environment
+ - requestAnimationFrame: best for browser UI (frame batching)
* - queueMicrotask: for headless or server tests (microtask batching)
* - setTimeout: generic fallback
*/
function pickScheduler() {
if (typeof requestAnimationFrame === 'function') return requestAnimationFrame;
if (typeof queueMicrotask === 'function') return queueMicrotask;
return (cb) => { setTimeout(cb, 0); };
}
// run notifications to all subscribers whose selected value changed
function runNotifications() {
scheduled = false;
const snapshot = state;
const arr = Array.from(subscribers); // snapshot avoids reentrancy
for (let i = 0; i < arr.length; i++) {
const sub = arr[i];
if (!sub.active) continue;
let next;
try {
next = sub.selector(snapshot);
} catch (err) {
console.error(err);
unsubscribe(sub);
continue;
}
if (valuesEqual(next, sub.prev)) continue;
sub.prev = next;
try {
sub.callback(next, false); // not first time
} catch (err) {
console.error(err);
unsubscribe(sub);
}
}
}
function valuesEqual(a, b) {
if (Object.is(a, b)) return true;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!Object.is(a[i], b[i])) return false;
}
return true;
}
if (isPlainObject(a) && isPlainObject(b)) {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
const k = keysA[i];
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
if (!Object.is(a[k], b[k])) return false;
}
return true;
}
// everything else (functions, dates, maps, etc.) compares by reference
return false;
}
function isPlainObject(x) {
if (x === null || typeof x !== 'object') return false;
const proto = Object.getPrototypeOf(x);
return proto === Object.prototype || proto === null;
}
return { get: get, set: set, patch: patch, subscribe: subscribe };
}
// expose factory
return { createStore: createStore };
});