@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/store.ts
CHANGED
|
@@ -1,82 +1,123 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isComputed } from './computed'
|
|
2
|
+
import {
|
|
3
|
+
type ArrayToRecord,
|
|
4
|
+
diff,
|
|
5
|
+
type UnknownArray,
|
|
6
|
+
type UnknownRecord,
|
|
7
|
+
type UnknownRecordOrArray,
|
|
8
|
+
} from './diff'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
InvalidSignalValueError,
|
|
12
|
+
NullishSignalValueError,
|
|
13
|
+
StoreKeyExistsError,
|
|
14
|
+
StoreKeyRangeError,
|
|
15
|
+
StoreKeyReadonlyError,
|
|
16
|
+
} from './errors'
|
|
17
|
+
import { isMutableSignal, type Signal } from './signal'
|
|
18
|
+
import { createState, isState, type State } from './state'
|
|
3
19
|
import {
|
|
4
20
|
batch,
|
|
5
21
|
type Cleanup,
|
|
22
|
+
createWatcher,
|
|
6
23
|
notify,
|
|
24
|
+
observe,
|
|
7
25
|
subscribe,
|
|
8
26
|
type Watcher,
|
|
9
|
-
} from './
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
} from './system'
|
|
28
|
+
import {
|
|
29
|
+
isFunction,
|
|
30
|
+
isObjectOfType,
|
|
31
|
+
isRecord,
|
|
32
|
+
isSymbol,
|
|
33
|
+
recordToArray,
|
|
34
|
+
UNSET,
|
|
35
|
+
valueString,
|
|
36
|
+
} from './util'
|
|
17
37
|
|
|
18
38
|
/* === Types === */
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
type: 'store-add'
|
|
22
|
-
detail: Partial<T>
|
|
23
|
-
}
|
|
40
|
+
type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
type StoreChanges<T> = {
|
|
43
|
+
add: Partial<T>
|
|
44
|
+
change: Partial<T>
|
|
45
|
+
remove: Partial<T>
|
|
46
|
+
sort: string[]
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
detail: Partial<T>
|
|
49
|
+
type StoreListeners<T> = {
|
|
50
|
+
[K in keyof StoreChanges<T>]: Set<(change: StoreChanges<T>[K]) => void>
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
'store-remove': StoreRemoveEvent<T>
|
|
53
|
+
interface BaseStore {
|
|
54
|
+
readonly [Symbol.toStringTag]: 'Store'
|
|
55
|
+
readonly size: State<number>
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
type RecordStore<T extends UnknownRecord> = BaseStore & {
|
|
59
|
+
[K in keyof T]: T[K] extends readonly unknown[] | Record<string, unknown>
|
|
60
|
+
? Store<T[K]>
|
|
61
|
+
: State<T[K]>
|
|
62
|
+
} & {
|
|
63
|
+
[Symbol.iterator](): IterableIterator<
|
|
64
|
+
[
|
|
65
|
+
Extract<keyof T, string>,
|
|
66
|
+
T[Extract<keyof T, string>] extends
|
|
67
|
+
| readonly unknown[]
|
|
68
|
+
| Record<string, unknown>
|
|
69
|
+
? Store<T[Extract<keyof T, string>]>
|
|
70
|
+
: State<T[Extract<keyof T, string>]>,
|
|
71
|
+
]
|
|
72
|
+
>
|
|
73
|
+
add<K extends Extract<keyof T, string>>(key: K, value: T[K]): void
|
|
74
|
+
get(): T
|
|
75
|
+
set(value: T): void
|
|
76
|
+
update(fn: (value: T) => T): void
|
|
77
|
+
sort<U = T[Extract<keyof T, string>]>(
|
|
78
|
+
compareFn?: (a: U, b: U) => number,
|
|
46
79
|
): void
|
|
47
|
-
|
|
48
|
-
type: string,
|
|
49
|
-
listener: EventListenerOrEventListenerObject,
|
|
50
|
-
options?: boolean | AddEventListenerOptions,
|
|
51
|
-
): void
|
|
52
|
-
|
|
53
|
-
removeEventListener<K extends keyof StoreEventMap<T>>(
|
|
80
|
+
on<K extends keyof StoreChanges<T>>(
|
|
54
81
|
type: K,
|
|
55
|
-
listener: (
|
|
56
|
-
|
|
57
|
-
): void
|
|
58
|
-
|
|
59
|
-
type: string,
|
|
60
|
-
listener: EventListenerOrEventListenerObject,
|
|
61
|
-
options?: boolean | EventListenerOptions,
|
|
62
|
-
): void
|
|
82
|
+
listener: (change: StoreChanges<T>[K]) => void,
|
|
83
|
+
): Cleanup
|
|
84
|
+
remove<K extends Extract<keyof T, string>>(key: K): void
|
|
85
|
+
}
|
|
63
86
|
|
|
64
|
-
|
|
87
|
+
type ArrayStore<T extends UnknownArray> = BaseStore & {
|
|
88
|
+
[Symbol.iterator](): IterableIterator<
|
|
89
|
+
ArrayItem<T> extends readonly unknown[] | Record<string, unknown>
|
|
90
|
+
? Store<ArrayItem<T>>
|
|
91
|
+
: State<ArrayItem<T>>
|
|
92
|
+
>
|
|
93
|
+
readonly [Symbol.isConcatSpreadable]: boolean
|
|
94
|
+
[n: number]: ArrayItem<T> extends
|
|
95
|
+
| readonly unknown[]
|
|
96
|
+
| Record<string, unknown>
|
|
97
|
+
? Store<ArrayItem<T>>
|
|
98
|
+
: State<ArrayItem<T>>
|
|
99
|
+
add(value: ArrayItem<T>): void
|
|
100
|
+
get(): T
|
|
101
|
+
set(value: T): void
|
|
102
|
+
update(fn: (value: T) => T): void
|
|
103
|
+
sort<U = ArrayItem<T>>(compareFn?: (a: U, b: U) => number): void
|
|
104
|
+
on<K extends keyof StoreChanges<T>>(
|
|
105
|
+
type: K,
|
|
106
|
+
listener: (change: StoreChanges<T>[K]) => void,
|
|
107
|
+
): Cleanup
|
|
108
|
+
remove(index: number): void
|
|
109
|
+
readonly length: number
|
|
65
110
|
}
|
|
66
111
|
|
|
67
|
-
type Store<T extends
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
set(value: T): void
|
|
77
|
-
update(updater: (value: T) => T): void
|
|
78
|
-
size: State<number>
|
|
79
|
-
}
|
|
112
|
+
type Store<T extends UnknownRecord | UnknownArray> = T extends UnknownRecord
|
|
113
|
+
? RecordStore<T>
|
|
114
|
+
: T extends UnknownArray
|
|
115
|
+
? ArrayStore<T>
|
|
116
|
+
: never
|
|
117
|
+
|
|
118
|
+
/* === Constants === */
|
|
119
|
+
|
|
120
|
+
const TYPE_STORE = 'Store'
|
|
80
121
|
|
|
81
122
|
/* === Functions === */
|
|
82
123
|
|
|
@@ -92,87 +133,178 @@ type Store<T extends UnknownRecordOrArray = UnknownRecord> = {
|
|
|
92
133
|
* @param {T} initialValue - initial object or array value of the store
|
|
93
134
|
* @returns {Store<T>} - new store with reactive properties that preserves the original type T
|
|
94
135
|
*/
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
136
|
+
const createStore = <T extends UnknownRecord | UnknownArray>(
|
|
137
|
+
initialValue: T,
|
|
138
|
+
): Store<T> => {
|
|
139
|
+
if (initialValue == null) throw new NullishSignalValueError('store')
|
|
140
|
+
|
|
141
|
+
const watchers = new Set<Watcher>()
|
|
142
|
+
const listeners: StoreListeners<T> = {
|
|
143
|
+
add: new Set<(change: Partial<T>) => void>(),
|
|
144
|
+
change: new Set<(change: Partial<T>) => void>(),
|
|
145
|
+
remove: new Set<(change: Partial<T>) => void>(),
|
|
146
|
+
sort: new Set<(change: string[]) => void>(),
|
|
147
|
+
}
|
|
148
|
+
const signals = new Map<string, Signal<T[Extract<keyof T, string>] & {}>>()
|
|
149
|
+
const signalWatchers = new Map<string, Watcher>()
|
|
150
|
+
|
|
151
|
+
// Determine if this is an array-like store at creation time
|
|
152
|
+
const isArrayLike = Array.isArray(initialValue)
|
|
101
153
|
|
|
102
154
|
// Internal state
|
|
103
|
-
const size =
|
|
104
|
-
|
|
105
|
-
// Get current record
|
|
106
|
-
const current = ()
|
|
107
|
-
const
|
|
108
|
-
const arrayIndexes = validArrayIndexes(keys)
|
|
109
|
-
if (arrayIndexes)
|
|
110
|
-
return arrayIndexes.map(index =>
|
|
111
|
-
signals.get(String(index))?.get(),
|
|
112
|
-
) as unknown as T
|
|
113
|
-
const record: Partial<T> = {}
|
|
155
|
+
const size = createState(0)
|
|
156
|
+
|
|
157
|
+
// Get current record
|
|
158
|
+
const current = () => {
|
|
159
|
+
const record: Record<string, unknown> = {}
|
|
114
160
|
for (const [key, signal] of signals) {
|
|
115
161
|
record[key] = signal.get()
|
|
116
162
|
}
|
|
117
|
-
return record
|
|
163
|
+
return record
|
|
118
164
|
}
|
|
119
165
|
|
|
120
|
-
// Emit
|
|
121
|
-
const emit =
|
|
122
|
-
|
|
166
|
+
// Emit change notifications
|
|
167
|
+
const emit = <K extends keyof StoreChanges<T>>(
|
|
168
|
+
key: K,
|
|
169
|
+
changes: StoreChanges<T>[K],
|
|
170
|
+
) => {
|
|
171
|
+
Object.freeze(changes)
|
|
172
|
+
for (const listener of listeners[key]) {
|
|
173
|
+
listener(changes)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Get sorted indexes
|
|
178
|
+
const getSortedIndexes = () =>
|
|
179
|
+
Array.from(signals.keys())
|
|
180
|
+
.map(k => Number(k))
|
|
181
|
+
.filter(n => Number.isInteger(n))
|
|
182
|
+
.sort((a, b) => a - b)
|
|
183
|
+
|
|
184
|
+
// Validate input
|
|
185
|
+
const isValidValue = <T>(
|
|
186
|
+
key: string,
|
|
187
|
+
value: T,
|
|
188
|
+
): value is NonNullable<T> => {
|
|
189
|
+
if (value == null)
|
|
190
|
+
throw new NullishSignalValueError(`store for key "${key}"`)
|
|
191
|
+
if (value === UNSET) return true
|
|
192
|
+
if (isSymbol(value) || isFunction(value) || isComputed(value))
|
|
193
|
+
throw new InvalidSignalValueError(
|
|
194
|
+
`store for key "${key}"`,
|
|
195
|
+
valueString(value),
|
|
196
|
+
)
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
123
199
|
|
|
124
200
|
// Add nested signal and effect
|
|
125
|
-
const addProperty = <K extends keyof T
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
const addProperty = <K extends Extract<keyof T, string>>(
|
|
202
|
+
key: K,
|
|
203
|
+
value: T[K] | ArrayItem<T>,
|
|
204
|
+
single = false,
|
|
205
|
+
): boolean => {
|
|
206
|
+
if (!isValidValue(key, value)) return false
|
|
207
|
+
const signal =
|
|
208
|
+
isState(value) || isStore(value)
|
|
209
|
+
? value
|
|
210
|
+
: isRecord(value) || Array.isArray(value)
|
|
211
|
+
? createStore(value)
|
|
212
|
+
: createState(value)
|
|
213
|
+
// @ts-expect-error non-matching signal types
|
|
214
|
+
signals.set(key, signal)
|
|
215
|
+
const watcher = createWatcher(() =>
|
|
216
|
+
observe(() => {
|
|
217
|
+
emit('change', {
|
|
218
|
+
[key]: signal.get(),
|
|
219
|
+
} as unknown as Partial<T>)
|
|
220
|
+
}, watcher),
|
|
221
|
+
)
|
|
222
|
+
watcher()
|
|
223
|
+
signalWatchers.set(key, watcher)
|
|
224
|
+
|
|
225
|
+
if (single) {
|
|
226
|
+
size.set(signals.size)
|
|
227
|
+
notify(watchers)
|
|
228
|
+
emit('add', {
|
|
229
|
+
[key]: value,
|
|
230
|
+
} as unknown as Partial<T>)
|
|
231
|
+
}
|
|
232
|
+
return true
|
|
135
233
|
}
|
|
136
234
|
|
|
137
235
|
// Remove nested signal and effect
|
|
138
|
-
const removeProperty = <K extends keyof T
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
236
|
+
const removeProperty = <K extends Extract<keyof T, string>>(
|
|
237
|
+
key: K,
|
|
238
|
+
single = false,
|
|
239
|
+
) => {
|
|
240
|
+
const ok = signals.delete(key)
|
|
241
|
+
if (ok) {
|
|
242
|
+
const watcher = signalWatchers.get(key)
|
|
243
|
+
if (watcher) watcher.cleanup()
|
|
244
|
+
signalWatchers.delete(key)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (single) {
|
|
248
|
+
size.set(signals.size)
|
|
249
|
+
notify(watchers)
|
|
250
|
+
emit('remove', {
|
|
251
|
+
[key]: UNSET,
|
|
252
|
+
} as unknown as Partial<T>)
|
|
253
|
+
}
|
|
254
|
+
return ok
|
|
144
255
|
}
|
|
145
256
|
|
|
146
257
|
// Reconcile data and dispatch events
|
|
147
|
-
const reconcile = (
|
|
148
|
-
|
|
258
|
+
const reconcile = (
|
|
259
|
+
oldValue: T,
|
|
260
|
+
newValue: T,
|
|
261
|
+
initialRun?: boolean,
|
|
262
|
+
): boolean => {
|
|
263
|
+
const changes = diff(
|
|
264
|
+
oldValue as T extends UnknownArray ? ArrayToRecord<T> : T,
|
|
265
|
+
newValue as T extends UnknownArray ? ArrayToRecord<T> : T,
|
|
266
|
+
)
|
|
149
267
|
|
|
150
268
|
batch(() => {
|
|
269
|
+
// Additions
|
|
151
270
|
if (Object.keys(changes.add).length) {
|
|
152
271
|
for (const key in changes.add) {
|
|
153
|
-
const value = changes.add[key]
|
|
154
|
-
|
|
272
|
+
const value = changes.add[key] ?? UNSET
|
|
273
|
+
addProperty(
|
|
274
|
+
key as Extract<keyof T, string>,
|
|
275
|
+
value as T[Extract<keyof T, string>] & {},
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Queue initial additions event to allow listeners to be added first
|
|
280
|
+
if (initialRun) {
|
|
281
|
+
setTimeout(() => {
|
|
282
|
+
emit('add', changes.add as Partial<T>)
|
|
283
|
+
}, 0)
|
|
284
|
+
} else {
|
|
285
|
+
emit('add', changes.add as Partial<T>)
|
|
155
286
|
}
|
|
156
|
-
emit('store-add', changes.add)
|
|
157
287
|
}
|
|
288
|
+
|
|
289
|
+
// Changes
|
|
158
290
|
if (Object.keys(changes.change).length) {
|
|
159
291
|
for (const key in changes.change) {
|
|
160
|
-
const signal = signals.get(key)
|
|
161
292
|
const value = changes.change[key]
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
293
|
+
if (!isValidValue(key, value)) continue
|
|
294
|
+
const signal = signals.get(key as Extract<keyof T, string>)
|
|
295
|
+
if (isMutableSignal(signal))
|
|
296
|
+
signal.set(value as T[Extract<keyof T, string>] & {})
|
|
297
|
+
else
|
|
298
|
+
throw new StoreKeyReadonlyError(key, valueString(value))
|
|
168
299
|
}
|
|
169
|
-
emit('
|
|
300
|
+
emit('change', changes.change as Partial<T>)
|
|
170
301
|
}
|
|
302
|
+
|
|
303
|
+
// Removals
|
|
171
304
|
if (Object.keys(changes.remove).length) {
|
|
172
|
-
for (const key in changes.remove)
|
|
173
|
-
removeProperty(key)
|
|
174
|
-
|
|
175
|
-
emit('store-remove', changes.remove)
|
|
305
|
+
for (const key in changes.remove)
|
|
306
|
+
removeProperty(key as Extract<keyof T, string>)
|
|
307
|
+
emit('remove', changes.remove as Partial<T>)
|
|
176
308
|
}
|
|
177
309
|
|
|
178
310
|
size.set(signals.size)
|
|
@@ -181,112 +313,190 @@ const store = <T extends UnknownRecordOrArray>(initialValue: T): Store<T> => {
|
|
|
181
313
|
return changes.changed
|
|
182
314
|
}
|
|
183
315
|
|
|
184
|
-
// Initialize data
|
|
185
|
-
reconcile({} as T, initialValue)
|
|
316
|
+
// Initialize data - convert arrays to records for internal storage
|
|
317
|
+
reconcile({} as T, initialValue, true)
|
|
186
318
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
319
|
+
// Methods and Properties
|
|
320
|
+
const store: Record<string, unknown> = {
|
|
321
|
+
add: isArrayLike
|
|
322
|
+
? (v: ArrayItem<T>): void => {
|
|
323
|
+
const nextIndex = signals.size
|
|
324
|
+
const key = String(nextIndex) as Extract<keyof T, string>
|
|
325
|
+
addProperty(key, v, true)
|
|
326
|
+
}
|
|
327
|
+
: <K extends Extract<keyof T, string>>(k: K, v: T[K]): void => {
|
|
328
|
+
if (!signals.has(k)) addProperty(k, v, true)
|
|
329
|
+
else throw new StoreKeyExistsError(k, valueString(v))
|
|
330
|
+
},
|
|
331
|
+
get: (): T => {
|
|
332
|
+
subscribe(watchers)
|
|
333
|
+
return recordToArray(current()) as T
|
|
334
|
+
},
|
|
335
|
+
remove: isArrayLike
|
|
336
|
+
? (index: number): void => {
|
|
337
|
+
const currentArray = recordToArray(current()) as T
|
|
338
|
+
const currentLength = signals.size
|
|
339
|
+
if (
|
|
340
|
+
!Array.isArray(currentArray) ||
|
|
341
|
+
index <= -currentLength ||
|
|
342
|
+
index >= currentLength
|
|
343
|
+
)
|
|
344
|
+
throw new StoreKeyRangeError(index)
|
|
345
|
+
const newArray = [...currentArray]
|
|
346
|
+
newArray.splice(index, 1)
|
|
347
|
+
|
|
348
|
+
if (reconcile(currentArray, newArray as unknown as T))
|
|
349
|
+
notify(watchers)
|
|
350
|
+
}
|
|
351
|
+
: <K extends Extract<keyof T, string>>(k: K): void => {
|
|
352
|
+
if (signals.has(k)) removeProperty(k, true)
|
|
353
|
+
},
|
|
354
|
+
set: (v: T): void => {
|
|
355
|
+
if (reconcile(current() as T, v)) {
|
|
356
|
+
notify(watchers)
|
|
357
|
+
if (UNSET === v) watchers.clear()
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
update: (fn: (v: T) => T): void => {
|
|
361
|
+
const oldValue = current()
|
|
362
|
+
const newValue = fn(recordToArray(oldValue) as T)
|
|
363
|
+
if (reconcile(oldValue as T, newValue)) {
|
|
364
|
+
notify(watchers)
|
|
365
|
+
if (UNSET === newValue) watchers.clear()
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
sort: (
|
|
369
|
+
compareFn?: <
|
|
370
|
+
U = T extends UnknownArray
|
|
371
|
+
? ArrayItem<T>
|
|
372
|
+
: T[Extract<keyof T, string>],
|
|
373
|
+
>(
|
|
374
|
+
a: U,
|
|
375
|
+
b: U,
|
|
376
|
+
) => number,
|
|
377
|
+
): void => {
|
|
378
|
+
// Get all entries as [key, value] pairs
|
|
379
|
+
const entries = Array.from(signals.entries())
|
|
380
|
+
.map(
|
|
381
|
+
([key, signal]) =>
|
|
382
|
+
[key, signal.get()] as [
|
|
383
|
+
string,
|
|
384
|
+
T[Extract<keyof T, string>],
|
|
385
|
+
],
|
|
386
|
+
)
|
|
387
|
+
.sort(
|
|
388
|
+
compareFn
|
|
389
|
+
? (a, b) => compareFn(a[1], b[1])
|
|
390
|
+
: (a, b) => String(a[1]).localeCompare(String(b[1])),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
// Create array of original keys in their new sorted order
|
|
394
|
+
const newOrder: string[] = entries.map(([key]) => String(key))
|
|
395
|
+
const newSignals = new Map<
|
|
396
|
+
string,
|
|
397
|
+
Signal<T[Extract<keyof T, string>] & {}>
|
|
398
|
+
>()
|
|
399
|
+
|
|
400
|
+
entries.forEach(([key], newIndex) => {
|
|
401
|
+
const oldKey = String(key)
|
|
402
|
+
const newKey = isArrayLike ? String(newIndex) : String(key)
|
|
403
|
+
|
|
404
|
+
const signal = signals.get(oldKey)
|
|
405
|
+
if (signal) newSignals.set(newKey, signal)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Replace signals map
|
|
409
|
+
signals.clear()
|
|
410
|
+
newSignals.forEach((signal, key) => signals.set(key, signal))
|
|
411
|
+
notify(watchers)
|
|
412
|
+
emit('sort', newOrder)
|
|
413
|
+
},
|
|
414
|
+
on: <K extends keyof StoreChanges<T>>(
|
|
415
|
+
type: K,
|
|
416
|
+
listener: (change: StoreChanges<T>[K]) => void,
|
|
417
|
+
): Cleanup => {
|
|
418
|
+
listeners[type].add(listener)
|
|
419
|
+
return () => listeners[type].delete(listener)
|
|
420
|
+
},
|
|
421
|
+
size,
|
|
422
|
+
}
|
|
206
423
|
|
|
207
424
|
// Return proxy directly with integrated signal methods
|
|
208
425
|
return new Proxy({} as Store<T>, {
|
|
209
426
|
get(_target, prop) {
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
case 'get':
|
|
224
|
-
return (): T => {
|
|
225
|
-
subscribe(watchers)
|
|
226
|
-
return current()
|
|
227
|
-
}
|
|
228
|
-
case 'remove':
|
|
229
|
-
return <K extends keyof T>(k: K): void => {
|
|
230
|
-
if (signals.has(k)) {
|
|
231
|
-
removeProperty(k)
|
|
232
|
-
notify(watchers)
|
|
233
|
-
emit('store-remove', { [k]: UNSET } as Partial<T>)
|
|
234
|
-
size.set(signals.size)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
case 'set':
|
|
238
|
-
return (v: T): void => {
|
|
239
|
-
if (reconcile(current(), v)) {
|
|
240
|
-
notify(watchers)
|
|
241
|
-
if (UNSET === v) watchers.clear()
|
|
427
|
+
// Symbols
|
|
428
|
+
if (prop === Symbol.toStringTag) return TYPE_STORE
|
|
429
|
+
if (prop === Symbol.isConcatSpreadable) return isArrayLike
|
|
430
|
+
if (prop === Symbol.iterator)
|
|
431
|
+
return isArrayLike
|
|
432
|
+
? function* () {
|
|
433
|
+
const indexes = getSortedIndexes()
|
|
434
|
+
for (const index of indexes) {
|
|
435
|
+
const signal = signals.get(
|
|
436
|
+
String(index) as Extract<keyof T, string>,
|
|
437
|
+
)
|
|
438
|
+
if (signal) yield signal
|
|
439
|
+
}
|
|
242
440
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const oldValue = current()
|
|
247
|
-
const newValue = fn(oldValue)
|
|
248
|
-
if (reconcile(oldValue, newValue)) {
|
|
249
|
-
notify(watchers)
|
|
250
|
-
if (UNSET === newValue) watchers.clear()
|
|
441
|
+
: function* () {
|
|
442
|
+
for (const [key, signal] of signals)
|
|
443
|
+
yield [key, signal]
|
|
251
444
|
}
|
|
252
|
-
|
|
253
|
-
case 'addEventListener':
|
|
254
|
-
return eventTarget.addEventListener.bind(eventTarget)
|
|
255
|
-
case 'removeEventListener':
|
|
256
|
-
return eventTarget.removeEventListener.bind(eventTarget)
|
|
257
|
-
case 'dispatchEvent':
|
|
258
|
-
return eventTarget.dispatchEvent.bind(eventTarget)
|
|
259
|
-
case 'size':
|
|
260
|
-
return size
|
|
261
|
-
}
|
|
445
|
+
if (isSymbol(prop)) return undefined
|
|
262
446
|
|
|
263
|
-
//
|
|
264
|
-
if (prop
|
|
265
|
-
if (prop ===
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
yield [key, signal]
|
|
269
|
-
}
|
|
270
|
-
}
|
|
447
|
+
// Methods and Properties
|
|
448
|
+
if (prop in store) return store[prop]
|
|
449
|
+
if (prop === 'length' && isArrayLike) {
|
|
450
|
+
subscribe(watchers)
|
|
451
|
+
return size.get()
|
|
271
452
|
}
|
|
272
453
|
|
|
273
|
-
//
|
|
274
|
-
return signals.get(
|
|
454
|
+
// Signals
|
|
455
|
+
return signals.get(prop as Extract<keyof T, string>)
|
|
275
456
|
},
|
|
276
457
|
has(_target, prop) {
|
|
277
|
-
const
|
|
458
|
+
const stringProp = String(prop)
|
|
278
459
|
return (
|
|
279
|
-
|
|
280
|
-
|
|
460
|
+
(stringProp &&
|
|
461
|
+
signals.has(stringProp as Extract<keyof T, string>)) ||
|
|
462
|
+
Object.keys(store).includes(stringProp) ||
|
|
281
463
|
prop === Symbol.toStringTag ||
|
|
282
|
-
prop === Symbol.iterator
|
|
464
|
+
prop === Symbol.iterator ||
|
|
465
|
+
prop === Symbol.isConcatSpreadable ||
|
|
466
|
+
(prop === 'length' && isArrayLike)
|
|
283
467
|
)
|
|
284
468
|
},
|
|
285
469
|
ownKeys() {
|
|
286
|
-
return
|
|
470
|
+
return isArrayLike
|
|
471
|
+
? getSortedIndexes()
|
|
472
|
+
.map(key => String(key))
|
|
473
|
+
.concat(['length'])
|
|
474
|
+
: Array.from(signals.keys()).map(key => String(key))
|
|
287
475
|
},
|
|
288
476
|
getOwnPropertyDescriptor(_target, prop) {
|
|
289
|
-
const
|
|
477
|
+
const nonEnumerable = <T>(value: T) => ({
|
|
478
|
+
enumerable: false,
|
|
479
|
+
configurable: true,
|
|
480
|
+
writable: false,
|
|
481
|
+
value,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
if (prop === 'length' && isArrayLike)
|
|
485
|
+
return {
|
|
486
|
+
enumerable: true,
|
|
487
|
+
configurable: true,
|
|
488
|
+
writable: false,
|
|
489
|
+
value: size.get(),
|
|
490
|
+
}
|
|
491
|
+
if (prop === Symbol.isConcatSpreadable)
|
|
492
|
+
return nonEnumerable(isArrayLike)
|
|
493
|
+
if (prop === Symbol.toStringTag) return nonEnumerable(TYPE_STORE)
|
|
494
|
+
if (isSymbol(prop)) return undefined
|
|
495
|
+
|
|
496
|
+
if (Object.keys(store).includes(prop))
|
|
497
|
+
return nonEnumerable(store[prop])
|
|
498
|
+
|
|
499
|
+
const signal = signals.get(prop as Extract<keyof T, string>)
|
|
290
500
|
return signal
|
|
291
501
|
? {
|
|
292
502
|
enumerable: true,
|
|
@@ -312,13 +522,4 @@ const isStore = <T extends UnknownRecordOrArray>(
|
|
|
312
522
|
|
|
313
523
|
/* === Exports === */
|
|
314
524
|
|
|
315
|
-
export {
|
|
316
|
-
TYPE_STORE,
|
|
317
|
-
isStore,
|
|
318
|
-
store,
|
|
319
|
-
type Store,
|
|
320
|
-
type StoreAddEvent,
|
|
321
|
-
type StoreChangeEvent,
|
|
322
|
-
type StoreRemoveEvent,
|
|
323
|
-
type StoreEventMap,
|
|
324
|
-
}
|
|
525
|
+
export { TYPE_STORE, isStore, createStore, type Store, type StoreChanges }
|