@zeix/cause-effect 0.15.0 → 0.15.2
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/README.md +40 -10
- package/index.dev.js +508 -382
- package/index.js +1 -1
- package/index.ts +26 -5
- package/package.json +1 -1
- package/src/computed.ts +2 -2
- package/src/diff.ts +70 -45
- package/src/effect.ts +3 -8
- package/src/errors.ts +56 -0
- package/src/match.ts +14 -19
- package/src/resolve.ts +8 -18
- package/src/signal.ts +38 -46
- package/src/state.ts +3 -2
- package/src/store.ts +410 -188
- package/src/util.ts +62 -20
- package/test/computed.test.ts +1 -1
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +1 -1
- package/test/match.test.ts +13 -3
- package/test/signal.test.ts +323 -0
- package/test/store.test.ts +971 -1
- package/types/index.d.ts +6 -5
- package/types/src/diff.d.ts +8 -3
- package/types/src/errors.d.ts +19 -0
- package/types/src/match.d.ts +4 -4
- package/types/src/resolve.d.ts +3 -3
- package/types/src/signal.d.ts +15 -18
- package/types/src/store.d.ts +56 -33
- package/types/src/util.d.ts +9 -7
- package/index.d.ts +0 -36
- package/types/test-new-effect.d.ts +0 -1
package/index.dev.js
CHANGED
|
@@ -1,3 +1,165 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
class CircularDependencyError extends Error {
|
|
3
|
+
constructor(where) {
|
|
4
|
+
super(`Circular dependency detected in ${where}`);
|
|
5
|
+
this.name = "CircularDependencyError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class InvalidSignalValueError extends TypeError {
|
|
10
|
+
constructor(where, value) {
|
|
11
|
+
super(`Invalid signal value ${value} in ${where}`);
|
|
12
|
+
this.name = "InvalidSignalValueError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class NullishSignalValueError extends TypeError {
|
|
17
|
+
constructor(where) {
|
|
18
|
+
super(`Nullish signal values are not allowed in ${where}`);
|
|
19
|
+
this.name = "NullishSignalValueError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class StoreKeyExistsError extends Error {
|
|
24
|
+
constructor(key, value) {
|
|
25
|
+
super(`Could not add store key "${key}" with value ${value} because it already exists`);
|
|
26
|
+
this.name = "StoreKeyExistsError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class StoreKeyRangeError extends RangeError {
|
|
31
|
+
constructor(index) {
|
|
32
|
+
super(`Could not remove store index ${String(index)} because it is out of range`);
|
|
33
|
+
this.name = "StoreKeyRangeError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class StoreKeyReadonlyError extends Error {
|
|
38
|
+
constructor(key, value) {
|
|
39
|
+
super(`Could not set store key "${key}" to ${value} because it is readonly`);
|
|
40
|
+
this.name = "StoreKeyReadonlyError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/util.ts
|
|
45
|
+
var UNSET = Symbol();
|
|
46
|
+
var isString = (value) => typeof value === "string";
|
|
47
|
+
var isNumber = (value) => typeof value === "number";
|
|
48
|
+
var isSymbol = (value) => typeof value === "symbol";
|
|
49
|
+
var isFunction = (fn) => typeof fn === "function";
|
|
50
|
+
var isAsyncFunction = (fn) => isFunction(fn) && fn.constructor.name === "AsyncFunction";
|
|
51
|
+
var isDefinedObject = (value) => !!value && typeof value === "object";
|
|
52
|
+
var isObjectOfType = (value, type) => Object.prototype.toString.call(value) === `[object ${type}]`;
|
|
53
|
+
var isRecord = (value) => isObjectOfType(value, "Object");
|
|
54
|
+
var isRecordOrArray = (value) => isRecord(value) || Array.isArray(value);
|
|
55
|
+
var validArrayIndexes = (keys) => {
|
|
56
|
+
if (!keys.length)
|
|
57
|
+
return null;
|
|
58
|
+
const indexes = keys.map((k) => isString(k) ? parseInt(k, 10) : isNumber(k) ? k : NaN);
|
|
59
|
+
return indexes.every((index) => Number.isFinite(index) && index >= 0) ? indexes.sort((a, b) => a - b) : null;
|
|
60
|
+
};
|
|
61
|
+
var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
|
|
62
|
+
var toError = (reason) => reason instanceof Error ? reason : Error(String(reason));
|
|
63
|
+
var recordToArray = (record) => {
|
|
64
|
+
const indexes = validArrayIndexes(Object.keys(record));
|
|
65
|
+
if (indexes === null)
|
|
66
|
+
return record;
|
|
67
|
+
const array = [];
|
|
68
|
+
for (const index of indexes) {
|
|
69
|
+
array.push(record[String(index)]);
|
|
70
|
+
}
|
|
71
|
+
return array;
|
|
72
|
+
};
|
|
73
|
+
var valueString = (value) => isString(value) ? `"${value}"` : isDefinedObject(value) ? JSON.stringify(value) : String(value);
|
|
74
|
+
|
|
75
|
+
// src/diff.ts
|
|
76
|
+
var isEqual = (a, b, visited) => {
|
|
77
|
+
if (Object.is(a, b))
|
|
78
|
+
return true;
|
|
79
|
+
if (typeof a !== typeof b)
|
|
80
|
+
return false;
|
|
81
|
+
if (typeof a !== "object" || a === null || b === null)
|
|
82
|
+
return false;
|
|
83
|
+
if (!visited)
|
|
84
|
+
visited = new WeakSet;
|
|
85
|
+
if (visited.has(a) || visited.has(b))
|
|
86
|
+
throw new CircularDependencyError("isEqual");
|
|
87
|
+
visited.add(a);
|
|
88
|
+
visited.add(b);
|
|
89
|
+
try {
|
|
90
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
91
|
+
if (a.length !== b.length)
|
|
92
|
+
return false;
|
|
93
|
+
for (let i = 0;i < a.length; i++) {
|
|
94
|
+
if (!isEqual(a[i], b[i], visited))
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
100
|
+
return false;
|
|
101
|
+
if (isRecord(a) && isRecord(b)) {
|
|
102
|
+
const aKeys = Object.keys(a);
|
|
103
|
+
const bKeys = Object.keys(b);
|
|
104
|
+
if (aKeys.length !== bKeys.length)
|
|
105
|
+
return false;
|
|
106
|
+
for (const key of aKeys) {
|
|
107
|
+
if (!(key in b))
|
|
108
|
+
return false;
|
|
109
|
+
if (!isEqual(a[key], b[key], visited))
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
} finally {
|
|
116
|
+
visited.delete(a);
|
|
117
|
+
visited.delete(b);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var diff = (oldObj, newObj) => {
|
|
121
|
+
const oldValid = isRecordOrArray(oldObj);
|
|
122
|
+
const newValid = isRecordOrArray(newObj);
|
|
123
|
+
if (!oldValid || !newValid) {
|
|
124
|
+
const changed2 = !Object.is(oldObj, newObj);
|
|
125
|
+
return {
|
|
126
|
+
changed: changed2,
|
|
127
|
+
add: changed2 && newValid ? newObj : {},
|
|
128
|
+
change: {},
|
|
129
|
+
remove: changed2 && oldValid ? oldObj : {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const visited = new WeakSet;
|
|
133
|
+
const add = {};
|
|
134
|
+
const change = {};
|
|
135
|
+
const remove = {};
|
|
136
|
+
const oldKeys = Object.keys(oldObj);
|
|
137
|
+
const newKeys = Object.keys(newObj);
|
|
138
|
+
const allKeys = new Set([...oldKeys, ...newKeys]);
|
|
139
|
+
for (const key of allKeys) {
|
|
140
|
+
const oldHas = key in oldObj;
|
|
141
|
+
const newHas = key in newObj;
|
|
142
|
+
if (!oldHas && newHas) {
|
|
143
|
+
add[key] = newObj[key];
|
|
144
|
+
continue;
|
|
145
|
+
} else if (oldHas && !newHas) {
|
|
146
|
+
remove[key] = UNSET;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const oldValue = oldObj[key];
|
|
150
|
+
const newValue = newObj[key];
|
|
151
|
+
if (!isEqual(oldValue, newValue, visited))
|
|
152
|
+
change[key] = newValue;
|
|
153
|
+
}
|
|
154
|
+
const changed = Object.keys(add).length > 0 || Object.keys(change).length > 0 || Object.keys(remove).length > 0;
|
|
155
|
+
return {
|
|
156
|
+
changed,
|
|
157
|
+
add,
|
|
158
|
+
change,
|
|
159
|
+
remove
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
1
163
|
// src/scheduler.ts
|
|
2
164
|
var active;
|
|
3
165
|
var pending = new Set;
|
|
@@ -87,371 +249,6 @@ var enqueue = (fn, dedupe) => new Promise((resolve, reject) => {
|
|
|
87
249
|
requestTick();
|
|
88
250
|
});
|
|
89
251
|
|
|
90
|
-
// src/util.ts
|
|
91
|
-
var isFunction = (fn) => typeof fn === "function";
|
|
92
|
-
var isAsyncFunction = (fn) => isFunction(fn) && fn.constructor.name === "AsyncFunction";
|
|
93
|
-
var isObjectOfType = (value, type) => Object.prototype.toString.call(value) === `[object ${type}]`;
|
|
94
|
-
var isRecord = (value) => isObjectOfType(value, "Object");
|
|
95
|
-
var arrayToRecord = (array) => {
|
|
96
|
-
const record = {};
|
|
97
|
-
for (let i = 0;i < array.length; i++) {
|
|
98
|
-
if (i in array)
|
|
99
|
-
record[String(i)] = array[i];
|
|
100
|
-
}
|
|
101
|
-
return record;
|
|
102
|
-
};
|
|
103
|
-
var hasMethod = (obj, methodName) => (methodName in obj) && isFunction(obj[methodName]);
|
|
104
|
-
var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
|
|
105
|
-
var toError = (reason) => reason instanceof Error ? reason : Error(String(reason));
|
|
106
|
-
|
|
107
|
-
class CircularDependencyError extends Error {
|
|
108
|
-
constructor(where) {
|
|
109
|
-
super(`Circular dependency in ${where} detected`);
|
|
110
|
-
this.name = "CircularDependencyError";
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// src/state.ts
|
|
115
|
-
var TYPE_STATE = "State";
|
|
116
|
-
var state = (initialValue) => {
|
|
117
|
-
const watchers = new Set;
|
|
118
|
-
let value = initialValue;
|
|
119
|
-
const s = {
|
|
120
|
-
[Symbol.toStringTag]: TYPE_STATE,
|
|
121
|
-
get: () => {
|
|
122
|
-
subscribe(watchers);
|
|
123
|
-
return value;
|
|
124
|
-
},
|
|
125
|
-
set: (v) => {
|
|
126
|
-
if (isEqual(value, v))
|
|
127
|
-
return;
|
|
128
|
-
value = v;
|
|
129
|
-
notify(watchers);
|
|
130
|
-
if (UNSET === value)
|
|
131
|
-
watchers.clear();
|
|
132
|
-
},
|
|
133
|
-
update: (fn) => {
|
|
134
|
-
s.set(fn(value));
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
return s;
|
|
138
|
-
};
|
|
139
|
-
var isState = (value) => isObjectOfType(value, TYPE_STATE);
|
|
140
|
-
|
|
141
|
-
// src/effect.ts
|
|
142
|
-
var effect = (callback) => {
|
|
143
|
-
const isAsync = isAsyncFunction(callback);
|
|
144
|
-
let running = false;
|
|
145
|
-
let controller;
|
|
146
|
-
const run = watch(() => observe(() => {
|
|
147
|
-
if (running)
|
|
148
|
-
throw new CircularDependencyError("effect");
|
|
149
|
-
running = true;
|
|
150
|
-
controller?.abort();
|
|
151
|
-
controller = undefined;
|
|
152
|
-
let cleanup;
|
|
153
|
-
try {
|
|
154
|
-
if (isAsync) {
|
|
155
|
-
controller = new AbortController;
|
|
156
|
-
const currentController = controller;
|
|
157
|
-
callback(controller.signal).then((cleanup2) => {
|
|
158
|
-
if (isFunction(cleanup2) && controller === currentController) {
|
|
159
|
-
run.off(cleanup2);
|
|
160
|
-
}
|
|
161
|
-
}).catch((error) => {
|
|
162
|
-
if (!isAbortError(error))
|
|
163
|
-
console.error("Async effect error:", error);
|
|
164
|
-
});
|
|
165
|
-
} else {
|
|
166
|
-
cleanup = callback();
|
|
167
|
-
if (isFunction(cleanup))
|
|
168
|
-
run.off(cleanup);
|
|
169
|
-
}
|
|
170
|
-
} catch (error) {
|
|
171
|
-
if (!isAbortError(error))
|
|
172
|
-
console.error("Effect callback error:", error);
|
|
173
|
-
}
|
|
174
|
-
running = false;
|
|
175
|
-
}, run));
|
|
176
|
-
run();
|
|
177
|
-
return () => {
|
|
178
|
-
controller?.abort();
|
|
179
|
-
run.cleanup();
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// src/store.ts
|
|
184
|
-
var TYPE_STORE = "Store";
|
|
185
|
-
var store = (initialValue) => {
|
|
186
|
-
const watchers = new Set;
|
|
187
|
-
const eventTarget = new EventTarget;
|
|
188
|
-
const signals = new Map;
|
|
189
|
-
const cleanups = new Map;
|
|
190
|
-
const size = state(0);
|
|
191
|
-
const current = () => {
|
|
192
|
-
const record = {};
|
|
193
|
-
for (const [key, value] of signals) {
|
|
194
|
-
record[key] = value.get();
|
|
195
|
-
}
|
|
196
|
-
return record;
|
|
197
|
-
};
|
|
198
|
-
const emit = (type, detail) => eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
|
|
199
|
-
const addSignalAndEffect = (key, value) => {
|
|
200
|
-
const signal = toMutableSignal(value);
|
|
201
|
-
signals.set(key, signal);
|
|
202
|
-
const cleanup = effect(() => {
|
|
203
|
-
const value2 = signal.get();
|
|
204
|
-
if (value2 != null)
|
|
205
|
-
emit("store-change", { [key]: value2 });
|
|
206
|
-
});
|
|
207
|
-
cleanups.set(key, cleanup);
|
|
208
|
-
};
|
|
209
|
-
const removeSignalAndEffect = (key) => {
|
|
210
|
-
signals.delete(key);
|
|
211
|
-
const cleanup = cleanups.get(key);
|
|
212
|
-
if (cleanup)
|
|
213
|
-
cleanup();
|
|
214
|
-
cleanups.delete(key);
|
|
215
|
-
};
|
|
216
|
-
const reconcile = (oldValue, newValue) => {
|
|
217
|
-
const changes = diff(oldValue, newValue);
|
|
218
|
-
batch(() => {
|
|
219
|
-
if (Object.keys(changes.add).length) {
|
|
220
|
-
for (const key in changes.add) {
|
|
221
|
-
const value = changes.add[key];
|
|
222
|
-
if (value != null)
|
|
223
|
-
addSignalAndEffect(key, value);
|
|
224
|
-
}
|
|
225
|
-
emit("store-add", changes.add);
|
|
226
|
-
}
|
|
227
|
-
if (Object.keys(changes.change).length) {
|
|
228
|
-
for (const key in changes.change) {
|
|
229
|
-
const signal = signals.get(key);
|
|
230
|
-
const value = changes.change[key];
|
|
231
|
-
if (signal && value != null && hasMethod(signal, "set"))
|
|
232
|
-
signal.set(value);
|
|
233
|
-
}
|
|
234
|
-
emit("store-change", changes.change);
|
|
235
|
-
}
|
|
236
|
-
if (Object.keys(changes.remove).length) {
|
|
237
|
-
for (const key in changes.remove) {
|
|
238
|
-
removeSignalAndEffect(key);
|
|
239
|
-
}
|
|
240
|
-
emit("store-remove", changes.remove);
|
|
241
|
-
}
|
|
242
|
-
size.set(signals.size);
|
|
243
|
-
});
|
|
244
|
-
return changes.changed;
|
|
245
|
-
};
|
|
246
|
-
reconcile({}, initialValue);
|
|
247
|
-
setTimeout(() => {
|
|
248
|
-
const initialAdditionsEvent = new CustomEvent("store-add", {
|
|
249
|
-
detail: initialValue
|
|
250
|
-
});
|
|
251
|
-
eventTarget.dispatchEvent(initialAdditionsEvent);
|
|
252
|
-
}, 0);
|
|
253
|
-
const storeProps = [
|
|
254
|
-
"add",
|
|
255
|
-
"get",
|
|
256
|
-
"remove",
|
|
257
|
-
"set",
|
|
258
|
-
"update",
|
|
259
|
-
"addEventListener",
|
|
260
|
-
"removeEventListener",
|
|
261
|
-
"dispatchEvent",
|
|
262
|
-
"size"
|
|
263
|
-
];
|
|
264
|
-
return new Proxy({}, {
|
|
265
|
-
get(_target, prop) {
|
|
266
|
-
const key = String(prop);
|
|
267
|
-
switch (prop) {
|
|
268
|
-
case "add":
|
|
269
|
-
return (k, v) => {
|
|
270
|
-
if (!signals.has(k)) {
|
|
271
|
-
addSignalAndEffect(k, v);
|
|
272
|
-
notify(watchers);
|
|
273
|
-
emit("store-add", {
|
|
274
|
-
[k]: v
|
|
275
|
-
});
|
|
276
|
-
size.set(signals.size);
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
case "get":
|
|
280
|
-
return () => {
|
|
281
|
-
subscribe(watchers);
|
|
282
|
-
return current();
|
|
283
|
-
};
|
|
284
|
-
case "remove":
|
|
285
|
-
return (k) => {
|
|
286
|
-
if (signals.has(k)) {
|
|
287
|
-
removeSignalAndEffect(k);
|
|
288
|
-
notify(watchers);
|
|
289
|
-
emit("store-remove", { [k]: UNSET });
|
|
290
|
-
size.set(signals.size);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
case "set":
|
|
294
|
-
return (v) => {
|
|
295
|
-
if (reconcile(current(), v)) {
|
|
296
|
-
notify(watchers);
|
|
297
|
-
if (UNSET === v)
|
|
298
|
-
watchers.clear();
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
case "update":
|
|
302
|
-
return (fn) => {
|
|
303
|
-
const oldValue = current();
|
|
304
|
-
const newValue = fn(oldValue);
|
|
305
|
-
if (reconcile(oldValue, newValue)) {
|
|
306
|
-
notify(watchers);
|
|
307
|
-
if (UNSET === newValue)
|
|
308
|
-
watchers.clear();
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
case "addEventListener":
|
|
312
|
-
return eventTarget.addEventListener.bind(eventTarget);
|
|
313
|
-
case "removeEventListener":
|
|
314
|
-
return eventTarget.removeEventListener.bind(eventTarget);
|
|
315
|
-
case "dispatchEvent":
|
|
316
|
-
return eventTarget.dispatchEvent.bind(eventTarget);
|
|
317
|
-
case "size":
|
|
318
|
-
return size;
|
|
319
|
-
}
|
|
320
|
-
if (prop === Symbol.toStringTag)
|
|
321
|
-
return TYPE_STORE;
|
|
322
|
-
if (prop === Symbol.iterator) {
|
|
323
|
-
return function* () {
|
|
324
|
-
for (const [key2, signal] of signals) {
|
|
325
|
-
yield [key2, signal];
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
return signals.get(key);
|
|
330
|
-
},
|
|
331
|
-
has(_target, prop) {
|
|
332
|
-
const key = String(prop);
|
|
333
|
-
return signals.has(key) || storeProps.includes(key) || prop === Symbol.toStringTag || prop === Symbol.iterator;
|
|
334
|
-
},
|
|
335
|
-
ownKeys() {
|
|
336
|
-
return Array.from(signals.keys());
|
|
337
|
-
},
|
|
338
|
-
getOwnPropertyDescriptor(_target, prop) {
|
|
339
|
-
const signal = signals.get(String(prop));
|
|
340
|
-
return signal ? {
|
|
341
|
-
enumerable: true,
|
|
342
|
-
configurable: true,
|
|
343
|
-
writable: true,
|
|
344
|
-
value: signal
|
|
345
|
-
} : undefined;
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
var isStore = (value) => isObjectOfType(value, TYPE_STORE);
|
|
350
|
-
|
|
351
|
-
// src/signal.ts
|
|
352
|
-
var UNSET = Symbol();
|
|
353
|
-
var isSignal = (value) => isState(value) || isComputed(value) || isStore(value);
|
|
354
|
-
function toSignal(value) {
|
|
355
|
-
if (isSignal(value))
|
|
356
|
-
return value;
|
|
357
|
-
if (isComputedCallback(value))
|
|
358
|
-
return computed(value);
|
|
359
|
-
if (Array.isArray(value))
|
|
360
|
-
return store(arrayToRecord(value));
|
|
361
|
-
if (isRecord(value))
|
|
362
|
-
return store(value);
|
|
363
|
-
return state(value);
|
|
364
|
-
}
|
|
365
|
-
function toMutableSignal(value) {
|
|
366
|
-
if (isState(value) || isStore(value))
|
|
367
|
-
return value;
|
|
368
|
-
if (Array.isArray(value))
|
|
369
|
-
return store(arrayToRecord(value));
|
|
370
|
-
if (isRecord(value))
|
|
371
|
-
return store(value);
|
|
372
|
-
return state(value);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// src/diff.ts
|
|
376
|
-
var isEqual = (a, b, visited) => {
|
|
377
|
-
if (Object.is(a, b))
|
|
378
|
-
return true;
|
|
379
|
-
if (typeof a !== typeof b)
|
|
380
|
-
return false;
|
|
381
|
-
if (typeof a !== "object" || a === null || b === null)
|
|
382
|
-
return false;
|
|
383
|
-
if (!visited)
|
|
384
|
-
visited = new WeakSet;
|
|
385
|
-
if (visited.has(a) || visited.has(b))
|
|
386
|
-
throw new CircularDependencyError("isEqual");
|
|
387
|
-
visited.add(a);
|
|
388
|
-
visited.add(b);
|
|
389
|
-
try {
|
|
390
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
391
|
-
if (a.length !== b.length)
|
|
392
|
-
return false;
|
|
393
|
-
for (let i = 0;i < a.length; i++) {
|
|
394
|
-
if (!isEqual(a[i], b[i], visited))
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
if (Array.isArray(a) !== Array.isArray(b))
|
|
400
|
-
return false;
|
|
401
|
-
if (isRecord(a) && isRecord(b)) {
|
|
402
|
-
const aKeys = Object.keys(a);
|
|
403
|
-
const bKeys = Object.keys(b);
|
|
404
|
-
if (aKeys.length !== bKeys.length)
|
|
405
|
-
return false;
|
|
406
|
-
for (const key of aKeys) {
|
|
407
|
-
if (!(key in b))
|
|
408
|
-
return false;
|
|
409
|
-
if (!isEqual(a[key], b[key], visited))
|
|
410
|
-
return false;
|
|
411
|
-
}
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
415
|
-
} finally {
|
|
416
|
-
visited.delete(a);
|
|
417
|
-
visited.delete(b);
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
var diff = (oldObj, newObj) => {
|
|
421
|
-
const visited = new WeakSet;
|
|
422
|
-
const diffRecords = (oldRecord, newRecord) => {
|
|
423
|
-
const add = {};
|
|
424
|
-
const change = {};
|
|
425
|
-
const remove = {};
|
|
426
|
-
const oldKeys = Object.keys(oldRecord);
|
|
427
|
-
const newKeys = Object.keys(newRecord);
|
|
428
|
-
const allKeys = new Set([...oldKeys, ...newKeys]);
|
|
429
|
-
for (const key of allKeys) {
|
|
430
|
-
const oldHas = key in oldRecord;
|
|
431
|
-
const newHas = key in newRecord;
|
|
432
|
-
if (!oldHas && newHas) {
|
|
433
|
-
add[key] = newRecord[key];
|
|
434
|
-
continue;
|
|
435
|
-
} else if (oldHas && !newHas) {
|
|
436
|
-
remove[key] = UNSET;
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
const oldValue = oldRecord[key];
|
|
440
|
-
const newValue = newRecord[key];
|
|
441
|
-
if (!isEqual(oldValue, newValue, visited))
|
|
442
|
-
change[key] = newValue;
|
|
443
|
-
}
|
|
444
|
-
const changed = Object.keys(add).length > 0 || Object.keys(change).length > 0 || Object.keys(remove).length > 0;
|
|
445
|
-
return {
|
|
446
|
-
changed,
|
|
447
|
-
add,
|
|
448
|
-
change,
|
|
449
|
-
remove
|
|
450
|
-
};
|
|
451
|
-
};
|
|
452
|
-
return diffRecords(oldObj, newObj);
|
|
453
|
-
};
|
|
454
|
-
|
|
455
252
|
// src/computed.ts
|
|
456
253
|
var TYPE_COMPUTED = "Computed";
|
|
457
254
|
var computed = (fn) => {
|
|
@@ -551,23 +348,60 @@ var computed = (fn) => {
|
|
|
551
348
|
};
|
|
552
349
|
var isComputed = (value) => isObjectOfType(value, TYPE_COMPUTED);
|
|
553
350
|
var isComputedCallback = (value) => isFunction(value) && value.length < 2;
|
|
351
|
+
// src/effect.ts
|
|
352
|
+
var effect = (callback) => {
|
|
353
|
+
const isAsync = isAsyncFunction(callback);
|
|
354
|
+
let running = false;
|
|
355
|
+
let controller;
|
|
356
|
+
const run = watch(() => observe(() => {
|
|
357
|
+
if (running)
|
|
358
|
+
throw new CircularDependencyError("effect");
|
|
359
|
+
running = true;
|
|
360
|
+
controller?.abort();
|
|
361
|
+
controller = undefined;
|
|
362
|
+
let cleanup;
|
|
363
|
+
try {
|
|
364
|
+
if (isAsync) {
|
|
365
|
+
controller = new AbortController;
|
|
366
|
+
const currentController = controller;
|
|
367
|
+
callback(controller.signal).then((cleanup2) => {
|
|
368
|
+
if (isFunction(cleanup2) && controller === currentController)
|
|
369
|
+
run.off(cleanup2);
|
|
370
|
+
}).catch((error) => {
|
|
371
|
+
if (!isAbortError(error))
|
|
372
|
+
console.error("Async effect error:", error);
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
cleanup = callback();
|
|
376
|
+
if (isFunction(cleanup))
|
|
377
|
+
run.off(cleanup);
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (!isAbortError(error))
|
|
381
|
+
console.error("Effect callback error:", error);
|
|
382
|
+
}
|
|
383
|
+
running = false;
|
|
384
|
+
}, run));
|
|
385
|
+
run();
|
|
386
|
+
return () => {
|
|
387
|
+
controller?.abort();
|
|
388
|
+
run.cleanup();
|
|
389
|
+
};
|
|
390
|
+
};
|
|
554
391
|
// src/match.ts
|
|
555
392
|
function match(result, handlers) {
|
|
556
393
|
try {
|
|
557
|
-
if (result.pending)
|
|
394
|
+
if (result.pending)
|
|
558
395
|
handlers.nil?.();
|
|
559
|
-
|
|
396
|
+
else if (result.errors)
|
|
560
397
|
handlers.err?.(result.errors);
|
|
561
|
-
|
|
562
|
-
handlers.ok
|
|
563
|
-
}
|
|
398
|
+
else if (result.ok)
|
|
399
|
+
handlers.ok(result.values);
|
|
564
400
|
} catch (error) {
|
|
565
|
-
if (handlers.err && (!result.errors || !result.errors.includes(toError(error))))
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
} else {
|
|
401
|
+
if (handlers.err && (!result.errors || !result.errors.includes(toError(error))))
|
|
402
|
+
handlers.err(result.errors ? [...result.errors, toError(error)] : [toError(error)]);
|
|
403
|
+
else
|
|
569
404
|
throw error;
|
|
570
|
-
}
|
|
571
405
|
}
|
|
572
406
|
}
|
|
573
407
|
// src/resolve.ts
|
|
@@ -578,23 +412,304 @@ function resolve(signals) {
|
|
|
578
412
|
for (const [key, signal] of Object.entries(signals)) {
|
|
579
413
|
try {
|
|
580
414
|
const value = signal.get();
|
|
581
|
-
if (value === UNSET)
|
|
415
|
+
if (value === UNSET)
|
|
582
416
|
pending2 = true;
|
|
583
|
-
|
|
417
|
+
else
|
|
584
418
|
values[key] = value;
|
|
585
|
-
}
|
|
586
419
|
} catch (e) {
|
|
587
420
|
errors.push(toError(e));
|
|
588
421
|
}
|
|
589
422
|
}
|
|
590
|
-
if (pending2)
|
|
423
|
+
if (pending2)
|
|
591
424
|
return { ok: false, pending: true };
|
|
592
|
-
|
|
593
|
-
if (errors.length > 0) {
|
|
425
|
+
if (errors.length > 0)
|
|
594
426
|
return { ok: false, errors };
|
|
595
|
-
}
|
|
596
427
|
return { ok: true, values };
|
|
597
428
|
}
|
|
429
|
+
// src/state.ts
|
|
430
|
+
var TYPE_STATE = "State";
|
|
431
|
+
var state = (initialValue) => {
|
|
432
|
+
const watchers = new Set;
|
|
433
|
+
let value = initialValue;
|
|
434
|
+
const s = {
|
|
435
|
+
[Symbol.toStringTag]: TYPE_STATE,
|
|
436
|
+
get: () => {
|
|
437
|
+
subscribe(watchers);
|
|
438
|
+
return value;
|
|
439
|
+
},
|
|
440
|
+
set: (v) => {
|
|
441
|
+
if (v == null)
|
|
442
|
+
throw new NullishSignalValueError("state");
|
|
443
|
+
if (isEqual(value, v))
|
|
444
|
+
return;
|
|
445
|
+
value = v;
|
|
446
|
+
notify(watchers);
|
|
447
|
+
if (UNSET === value)
|
|
448
|
+
watchers.clear();
|
|
449
|
+
},
|
|
450
|
+
update: (fn) => {
|
|
451
|
+
s.set(fn(value));
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
return s;
|
|
455
|
+
};
|
|
456
|
+
var isState = (value) => isObjectOfType(value, TYPE_STATE);
|
|
457
|
+
|
|
458
|
+
// src/store.ts
|
|
459
|
+
var TYPE_STORE = "Store";
|
|
460
|
+
var STORE_EVENT_ADD = "store-add";
|
|
461
|
+
var STORE_EVENT_CHANGE = "store-change";
|
|
462
|
+
var STORE_EVENT_REMOVE = "store-remove";
|
|
463
|
+
var STORE_EVENT_SORT = "store-sort";
|
|
464
|
+
var store = (initialValue) => {
|
|
465
|
+
const watchers = new Set;
|
|
466
|
+
const eventTarget = new EventTarget;
|
|
467
|
+
const signals = new Map;
|
|
468
|
+
const cleanups = new Map;
|
|
469
|
+
const isArrayLike = Array.isArray(initialValue);
|
|
470
|
+
const size = state(0);
|
|
471
|
+
const current = () => {
|
|
472
|
+
const record = {};
|
|
473
|
+
for (const [key, signal] of signals) {
|
|
474
|
+
record[key] = signal.get();
|
|
475
|
+
}
|
|
476
|
+
return record;
|
|
477
|
+
};
|
|
478
|
+
const emit = (type, detail) => eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
|
|
479
|
+
const getSortedIndexes = () => Array.from(signals.keys()).map((k) => Number(k)).filter((n) => Number.isInteger(n)).sort((a, b) => a - b);
|
|
480
|
+
const isValidValue = (key, value) => {
|
|
481
|
+
if (value == null)
|
|
482
|
+
throw new NullishSignalValueError(`store for key "${key}"`);
|
|
483
|
+
if (value === UNSET)
|
|
484
|
+
return true;
|
|
485
|
+
if (isSymbol(value) || isFunction(value) || isComputed(value))
|
|
486
|
+
throw new InvalidSignalValueError(`store for key "${key}"`, valueString(value));
|
|
487
|
+
return true;
|
|
488
|
+
};
|
|
489
|
+
const addProperty = (key, value, single = false) => {
|
|
490
|
+
if (!isValidValue(key, value))
|
|
491
|
+
return false;
|
|
492
|
+
const signal = isState(value) || isStore(value) ? value : isRecord(value) ? store(value) : Array.isArray(value) ? store(value) : state(value);
|
|
493
|
+
signals.set(key, signal);
|
|
494
|
+
const cleanup = effect(() => {
|
|
495
|
+
const currentValue = signal.get();
|
|
496
|
+
if (currentValue != null)
|
|
497
|
+
emit(STORE_EVENT_CHANGE, {
|
|
498
|
+
[key]: currentValue
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
cleanups.set(key, cleanup);
|
|
502
|
+
if (single) {
|
|
503
|
+
size.set(signals.size);
|
|
504
|
+
notify(watchers);
|
|
505
|
+
emit(STORE_EVENT_ADD, {
|
|
506
|
+
[key]: value
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
};
|
|
511
|
+
const removeProperty = (key, single = false) => {
|
|
512
|
+
const ok = signals.delete(key);
|
|
513
|
+
if (ok) {
|
|
514
|
+
const cleanup = cleanups.get(key);
|
|
515
|
+
if (cleanup)
|
|
516
|
+
cleanup();
|
|
517
|
+
cleanups.delete(key);
|
|
518
|
+
}
|
|
519
|
+
if (single) {
|
|
520
|
+
size.set(signals.size);
|
|
521
|
+
notify(watchers);
|
|
522
|
+
emit(STORE_EVENT_REMOVE, {
|
|
523
|
+
[key]: UNSET
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return ok;
|
|
527
|
+
};
|
|
528
|
+
const reconcile = (oldValue, newValue, initialRun) => {
|
|
529
|
+
const changes = diff(oldValue, newValue);
|
|
530
|
+
batch(() => {
|
|
531
|
+
if (Object.keys(changes.add).length) {
|
|
532
|
+
for (const key in changes.add) {
|
|
533
|
+
const value = changes.add[key] ?? UNSET;
|
|
534
|
+
addProperty(key, value);
|
|
535
|
+
}
|
|
536
|
+
if (initialRun) {
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
emit(STORE_EVENT_ADD, changes.add);
|
|
539
|
+
}, 0);
|
|
540
|
+
} else {
|
|
541
|
+
emit(STORE_EVENT_ADD, changes.add);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (Object.keys(changes.change).length) {
|
|
545
|
+
for (const key in changes.change) {
|
|
546
|
+
const value = changes.change[key];
|
|
547
|
+
if (!isValidValue(key, value))
|
|
548
|
+
continue;
|
|
549
|
+
const signal = signals.get(key);
|
|
550
|
+
if (isMutableSignal(signal))
|
|
551
|
+
signal.set(value);
|
|
552
|
+
else
|
|
553
|
+
throw new StoreKeyReadonlyError(key, valueString(value));
|
|
554
|
+
}
|
|
555
|
+
emit(STORE_EVENT_CHANGE, changes.change);
|
|
556
|
+
}
|
|
557
|
+
if (Object.keys(changes.remove).length) {
|
|
558
|
+
for (const key in changes.remove)
|
|
559
|
+
removeProperty(key);
|
|
560
|
+
emit(STORE_EVENT_REMOVE, changes.remove);
|
|
561
|
+
}
|
|
562
|
+
size.set(signals.size);
|
|
563
|
+
});
|
|
564
|
+
return changes.changed;
|
|
565
|
+
};
|
|
566
|
+
reconcile({}, initialValue, true);
|
|
567
|
+
const s = {
|
|
568
|
+
add: isArrayLike ? (v) => {
|
|
569
|
+
const nextIndex = signals.size;
|
|
570
|
+
const key = String(nextIndex);
|
|
571
|
+
addProperty(key, v, true);
|
|
572
|
+
} : (k, v) => {
|
|
573
|
+
if (!signals.has(k))
|
|
574
|
+
addProperty(k, v, true);
|
|
575
|
+
else
|
|
576
|
+
throw new StoreKeyExistsError(k, valueString(v));
|
|
577
|
+
},
|
|
578
|
+
get: () => {
|
|
579
|
+
subscribe(watchers);
|
|
580
|
+
return recordToArray(current());
|
|
581
|
+
},
|
|
582
|
+
remove: isArrayLike ? (index) => {
|
|
583
|
+
const currentArray = recordToArray(current());
|
|
584
|
+
const currentLength = signals.size;
|
|
585
|
+
if (!Array.isArray(currentArray) || index <= -currentLength || index >= currentLength)
|
|
586
|
+
throw new StoreKeyRangeError(index);
|
|
587
|
+
const newArray = [...currentArray];
|
|
588
|
+
newArray.splice(index, 1);
|
|
589
|
+
if (reconcile(currentArray, newArray))
|
|
590
|
+
notify(watchers);
|
|
591
|
+
} : (k) => {
|
|
592
|
+
if (signals.has(k))
|
|
593
|
+
removeProperty(k, true);
|
|
594
|
+
},
|
|
595
|
+
set: (v) => {
|
|
596
|
+
if (reconcile(current(), v)) {
|
|
597
|
+
notify(watchers);
|
|
598
|
+
if (UNSET === v)
|
|
599
|
+
watchers.clear();
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
update: (fn) => {
|
|
603
|
+
const oldValue = current();
|
|
604
|
+
const newValue = fn(recordToArray(oldValue));
|
|
605
|
+
if (reconcile(oldValue, newValue)) {
|
|
606
|
+
notify(watchers);
|
|
607
|
+
if (UNSET === newValue)
|
|
608
|
+
watchers.clear();
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
sort: (compareFn) => {
|
|
612
|
+
const entries = Array.from(signals.entries()).map(([key, signal]) => [key, signal.get()]).sort(compareFn ? (a, b) => compareFn(a[1], b[1]) : (a, b) => String(a[1]).localeCompare(String(b[1])));
|
|
613
|
+
const newOrder = entries.map(([key]) => String(key));
|
|
614
|
+
const newSignals = new Map;
|
|
615
|
+
entries.forEach(([key], newIndex) => {
|
|
616
|
+
const oldKey = String(key);
|
|
617
|
+
const newKey = isArrayLike ? String(newIndex) : String(key);
|
|
618
|
+
const signal = signals.get(oldKey);
|
|
619
|
+
if (signal)
|
|
620
|
+
newSignals.set(newKey, signal);
|
|
621
|
+
});
|
|
622
|
+
signals.clear();
|
|
623
|
+
newSignals.forEach((signal, key) => signals.set(key, signal));
|
|
624
|
+
notify(watchers);
|
|
625
|
+
emit(STORE_EVENT_SORT, newOrder);
|
|
626
|
+
},
|
|
627
|
+
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
|
628
|
+
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
|
629
|
+
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
|
630
|
+
size
|
|
631
|
+
};
|
|
632
|
+
return new Proxy({}, {
|
|
633
|
+
get(_target, prop) {
|
|
634
|
+
if (prop === Symbol.toStringTag)
|
|
635
|
+
return TYPE_STORE;
|
|
636
|
+
if (prop === Symbol.isConcatSpreadable)
|
|
637
|
+
return isArrayLike;
|
|
638
|
+
if (prop === Symbol.iterator)
|
|
639
|
+
return isArrayLike ? function* () {
|
|
640
|
+
const indexes = getSortedIndexes();
|
|
641
|
+
for (const index of indexes) {
|
|
642
|
+
const signal = signals.get(String(index));
|
|
643
|
+
if (signal)
|
|
644
|
+
yield signal;
|
|
645
|
+
}
|
|
646
|
+
} : function* () {
|
|
647
|
+
for (const [key, signal] of signals)
|
|
648
|
+
yield [key, signal];
|
|
649
|
+
};
|
|
650
|
+
if (isSymbol(prop))
|
|
651
|
+
return;
|
|
652
|
+
if (prop in s)
|
|
653
|
+
return s[prop];
|
|
654
|
+
if (prop === "length" && isArrayLike) {
|
|
655
|
+
subscribe(watchers);
|
|
656
|
+
return size.get();
|
|
657
|
+
}
|
|
658
|
+
return signals.get(prop);
|
|
659
|
+
},
|
|
660
|
+
has(_target, prop) {
|
|
661
|
+
const stringProp = String(prop);
|
|
662
|
+
return stringProp && signals.has(stringProp) || Object.keys(s).includes(stringProp) || prop === Symbol.toStringTag || prop === Symbol.iterator || prop === Symbol.isConcatSpreadable || prop === "length" && isArrayLike;
|
|
663
|
+
},
|
|
664
|
+
ownKeys() {
|
|
665
|
+
return isArrayLike ? getSortedIndexes().map((key) => String(key)).concat(["length"]) : Array.from(signals.keys()).map((key) => String(key));
|
|
666
|
+
},
|
|
667
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
668
|
+
const nonEnumerable = (value) => ({
|
|
669
|
+
enumerable: false,
|
|
670
|
+
configurable: true,
|
|
671
|
+
writable: false,
|
|
672
|
+
value
|
|
673
|
+
});
|
|
674
|
+
if (prop === "length" && isArrayLike)
|
|
675
|
+
return {
|
|
676
|
+
enumerable: true,
|
|
677
|
+
configurable: true,
|
|
678
|
+
writable: false,
|
|
679
|
+
value: size.get()
|
|
680
|
+
};
|
|
681
|
+
if (prop === Symbol.isConcatSpreadable)
|
|
682
|
+
return nonEnumerable(isArrayLike);
|
|
683
|
+
if (prop === Symbol.toStringTag)
|
|
684
|
+
return nonEnumerable(TYPE_STORE);
|
|
685
|
+
if (isSymbol(prop))
|
|
686
|
+
return;
|
|
687
|
+
if (Object.keys(s).includes(prop))
|
|
688
|
+
return nonEnumerable(s[prop]);
|
|
689
|
+
const signal = signals.get(prop);
|
|
690
|
+
return signal ? {
|
|
691
|
+
enumerable: true,
|
|
692
|
+
configurable: true,
|
|
693
|
+
writable: true,
|
|
694
|
+
value: signal
|
|
695
|
+
} : undefined;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
};
|
|
699
|
+
var isStore = (value) => isObjectOfType(value, TYPE_STORE);
|
|
700
|
+
|
|
701
|
+
// src/signal.ts
|
|
702
|
+
var isSignal = (value) => isState(value) || isComputed(value) || isStore(value);
|
|
703
|
+
var isMutableSignal = (value) => isState(value) || isStore(value);
|
|
704
|
+
function toSignal(value) {
|
|
705
|
+
if (isSignal(value))
|
|
706
|
+
return value;
|
|
707
|
+
if (isComputedCallback(value))
|
|
708
|
+
return computed(value);
|
|
709
|
+
if (Array.isArray(value) || isRecord(value))
|
|
710
|
+
return store(value);
|
|
711
|
+
return state(value);
|
|
712
|
+
}
|
|
598
713
|
export {
|
|
599
714
|
watch,
|
|
600
715
|
toSignal,
|
|
@@ -606,9 +721,15 @@ export {
|
|
|
606
721
|
observe,
|
|
607
722
|
notify,
|
|
608
723
|
match,
|
|
724
|
+
isSymbol,
|
|
725
|
+
isString,
|
|
609
726
|
isStore,
|
|
610
727
|
isState,
|
|
611
728
|
isSignal,
|
|
729
|
+
isRecordOrArray,
|
|
730
|
+
isRecord,
|
|
731
|
+
isNumber,
|
|
732
|
+
isMutableSignal,
|
|
612
733
|
isFunction,
|
|
613
734
|
isEqual,
|
|
614
735
|
isComputedCallback,
|
|
@@ -625,5 +746,10 @@ export {
|
|
|
625
746
|
TYPE_STORE,
|
|
626
747
|
TYPE_STATE,
|
|
627
748
|
TYPE_COMPUTED,
|
|
749
|
+
StoreKeyReadonlyError,
|
|
750
|
+
StoreKeyRangeError,
|
|
751
|
+
StoreKeyExistsError,
|
|
752
|
+
NullishSignalValueError,
|
|
753
|
+
InvalidSignalValueError,
|
|
628
754
|
CircularDependencyError
|
|
629
755
|
};
|