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