@zeix/cause-effect 0.15.1 → 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 +39 -1
- package/index.dev.js +517 -392
- package/index.js +1 -1
- package/index.ts +15 -4
- package/package.json +1 -1
- package/src/computed.ts +2 -2
- package/src/diff.ts +57 -44
- package/src/effect.ts +2 -6
- package/src/errors.ts +56 -0
- package/src/match.ts +2 -2
- package/src/resolve.ts +2 -2
- package/src/signal.ts +21 -43
- package/src/state.ts +3 -2
- package/src/store.ts +402 -179
- package/src/util.ts +50 -6
- 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 +12 -140
- package/test/store.test.ts +963 -20
- package/types/index.d.ts +5 -4
- package/types/src/diff.d.ts +5 -3
- package/types/src/errors.d.ts +19 -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/store.d.ts +48 -30
- package/types/src/util.d.ts +8 -5
- 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,76 +21,133 @@ 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
|
-
}
|
|
24
|
-
|
|
25
|
-
interface StoreChangeEvent<T extends UnknownRecordOrArray> extends CustomEvent {
|
|
26
|
-
type: 'store-change'
|
|
27
|
-
detail: Partial<T>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface StoreRemoveEvent<T extends UnknownRecordOrArray> extends CustomEvent {
|
|
31
|
-
type: 'store-remove'
|
|
32
|
-
detail: Partial<T>
|
|
33
|
-
}
|
|
38
|
+
type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
|
|
34
39
|
|
|
35
|
-
type StoreEventMap<T extends
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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>
|
|
80
|
+
? Store<T[K]>
|
|
81
|
+
: State<T[K]>
|
|
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'
|
|
80
151
|
|
|
81
152
|
/* === Functions === */
|
|
82
153
|
|
|
@@ -92,87 +163,165 @@ type Store<T extends UnknownRecordOrArray = UnknownRecord> = {
|
|
|
92
163
|
* @param {T} initialValue - initial object or array value of the store
|
|
93
164
|
* @returns {Store<T>} - new store with reactive properties that preserves the original type T
|
|
94
165
|
*/
|
|
95
|
-
const store = <T extends
|
|
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
|
-
|
|
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)
|
|
101
176
|
|
|
102
177
|
// Internal state
|
|
103
178
|
const size = state(0)
|
|
104
179
|
|
|
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> = {}
|
|
180
|
+
// Get current record
|
|
181
|
+
const current = () => {
|
|
182
|
+
const record: Record<string, unknown> = {}
|
|
114
183
|
for (const [key, signal] of signals) {
|
|
115
184
|
record[key] = signal.get()
|
|
116
185
|
}
|
|
117
|
-
return record
|
|
186
|
+
return record
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
// Emit event
|
|
121
|
-
const emit = (type: keyof StoreEventMap<T>, detail:
|
|
190
|
+
const emit = <R>(type: keyof StoreEventMap<T>, detail: R) =>
|
|
122
191
|
eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
|
|
123
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
|
+
|
|
124
216
|
// Add nested signal and effect
|
|
125
|
-
const addProperty = <K extends keyof T
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
217
|
+
const addProperty = <K extends Extract<keyof T, string>>(
|
|
218
|
+
key: K,
|
|
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
|
|
232
|
+
signals.set(key, signal)
|
|
129
233
|
const cleanup = effect(() => {
|
|
130
234
|
const currentValue = signal.get()
|
|
131
235
|
if (currentValue != null)
|
|
132
|
-
emit(
|
|
236
|
+
emit(STORE_EVENT_CHANGE, {
|
|
237
|
+
[key]: currentValue,
|
|
238
|
+
} as unknown as Partial<T>)
|
|
133
239
|
})
|
|
134
|
-
cleanups.set(
|
|
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
|
|
135
250
|
}
|
|
136
251
|
|
|
137
252
|
// Remove nested signal and effect
|
|
138
|
-
const removeProperty = <K extends keyof T
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
144
272
|
}
|
|
145
273
|
|
|
146
274
|
// Reconcile data and dispatch events
|
|
147
|
-
const reconcile = (
|
|
148
|
-
|
|
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
|
+
)
|
|
149
284
|
|
|
150
285
|
batch(() => {
|
|
286
|
+
// Additions
|
|
151
287
|
if (Object.keys(changes.add).length) {
|
|
152
288
|
for (const key in changes.add) {
|
|
153
|
-
const value = changes.add[key]
|
|
154
|
-
|
|
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>)
|
|
155
303
|
}
|
|
156
|
-
emit('store-add', changes.add)
|
|
157
304
|
}
|
|
305
|
+
|
|
306
|
+
// Changes
|
|
158
307
|
if (Object.keys(changes.change).length) {
|
|
159
308
|
for (const key in changes.change) {
|
|
160
|
-
const signal = signals.get(key)
|
|
161
309
|
const value = changes.change[key]
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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))
|
|
168
316
|
}
|
|
169
|
-
emit(
|
|
317
|
+
emit(STORE_EVENT_CHANGE, changes.change as Partial<T>)
|
|
170
318
|
}
|
|
319
|
+
|
|
320
|
+
// Removals
|
|
171
321
|
if (Object.keys(changes.remove).length) {
|
|
172
|
-
for (const key in changes.remove)
|
|
173
|
-
removeProperty(key)
|
|
174
|
-
|
|
175
|
-
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>)
|
|
176
325
|
}
|
|
177
326
|
|
|
178
327
|
size.set(signals.size)
|
|
@@ -181,112 +330,185 @@ const store = <T extends UnknownRecordOrArray>(initialValue: T): Store<T> => {
|
|
|
181
330
|
return changes.changed
|
|
182
331
|
}
|
|
183
332
|
|
|
184
|
-
// Initialize data
|
|
185
|
-
reconcile({} as T, initialValue)
|
|
333
|
+
// Initialize data - convert arrays to records for internal storage
|
|
334
|
+
reconcile({} as T, initialValue, true)
|
|
186
335
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
}
|
|
206
436
|
|
|
207
437
|
// Return proxy directly with integrated signal methods
|
|
208
438
|
return new Proxy({} as Store<T>, {
|
|
209
439
|
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()
|
|
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
|
+
}
|
|
242
453
|
}
|
|
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()
|
|
454
|
+
: function* () {
|
|
455
|
+
for (const [key, signal] of signals)
|
|
456
|
+
yield [key, signal]
|
|
251
457
|
}
|
|
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
|
-
}
|
|
458
|
+
if (isSymbol(prop)) return undefined
|
|
262
459
|
|
|
263
|
-
//
|
|
264
|
-
if (prop
|
|
265
|
-
if (prop ===
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
yield [key, signal]
|
|
269
|
-
}
|
|
270
|
-
}
|
|
460
|
+
// Methods and Properties
|
|
461
|
+
if (prop in s) return s[prop]
|
|
462
|
+
if (prop === 'length' && isArrayLike) {
|
|
463
|
+
subscribe(watchers)
|
|
464
|
+
return size.get()
|
|
271
465
|
}
|
|
272
466
|
|
|
273
|
-
//
|
|
274
|
-
return signals.get(
|
|
467
|
+
// Signals
|
|
468
|
+
return signals.get(prop as Extract<keyof T, string>)
|
|
275
469
|
},
|
|
276
470
|
has(_target, prop) {
|
|
277
|
-
const
|
|
471
|
+
const stringProp = String(prop)
|
|
278
472
|
return (
|
|
279
|
-
|
|
280
|
-
|
|
473
|
+
(stringProp &&
|
|
474
|
+
signals.has(stringProp as Extract<keyof T, string>)) ||
|
|
475
|
+
Object.keys(s).includes(stringProp) ||
|
|
281
476
|
prop === Symbol.toStringTag ||
|
|
282
|
-
prop === Symbol.iterator
|
|
477
|
+
prop === Symbol.iterator ||
|
|
478
|
+
prop === Symbol.isConcatSpreadable ||
|
|
479
|
+
(prop === 'length' && isArrayLike)
|
|
283
480
|
)
|
|
284
481
|
},
|
|
285
482
|
ownKeys() {
|
|
286
|
-
return
|
|
483
|
+
return isArrayLike
|
|
484
|
+
? getSortedIndexes()
|
|
485
|
+
.map(key => String(key))
|
|
486
|
+
.concat(['length'])
|
|
487
|
+
: Array.from(signals.keys()).map(key => String(key))
|
|
287
488
|
},
|
|
288
489
|
getOwnPropertyDescriptor(_target, prop) {
|
|
289
|
-
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>)
|
|
290
512
|
return signal
|
|
291
513
|
? {
|
|
292
514
|
enumerable: true,
|
|
@@ -320,5 +542,6 @@ export {
|
|
|
320
542
|
type StoreAddEvent,
|
|
321
543
|
type StoreChangeEvent,
|
|
322
544
|
type StoreRemoveEvent,
|
|
545
|
+
type StoreSortEvent,
|
|
323
546
|
type StoreEventMap,
|
|
324
547
|
}
|