@zeix/cause-effect 0.16.1 → 0.17.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 +71 -21
- package/.cursorrules +3 -2
- package/.github/copilot-instructions.md +59 -13
- package/CLAUDE.md +170 -24
- package/LICENSE +1 -1
- package/README.md +156 -52
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +19 -19
- package/archive/list.ts +551 -0
- package/archive/memo.ts +138 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +899 -503
- package/index.js +1 -1
- package/index.ts +41 -22
- package/package.json +1 -1
- package/src/classes/collection.ts +272 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +304 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +50 -25
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -30
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +796 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +10 -8
- package/types/src/classes/collection.d.ts +32 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +12 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -3
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/test/store.test.ts
CHANGED
|
@@ -1,262 +1,147 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
BaseStore,
|
|
4
4
|
createEffect,
|
|
5
|
-
createState,
|
|
6
5
|
createStore,
|
|
7
6
|
isStore,
|
|
8
|
-
|
|
7
|
+
Memo,
|
|
8
|
+
State,
|
|
9
9
|
UNSET,
|
|
10
|
-
} from '
|
|
10
|
+
} from '../index.ts'
|
|
11
11
|
|
|
12
12
|
describe('store', () => {
|
|
13
13
|
describe('creation and basic operations', () => {
|
|
14
|
+
test('creates BaseStore with initial values', () => {
|
|
15
|
+
const user = new BaseStore({
|
|
16
|
+
name: 'Hannah',
|
|
17
|
+
email: 'hannah@example.com',
|
|
18
|
+
})
|
|
19
|
+
expect(user.byKey('name').get()).toBe('Hannah')
|
|
20
|
+
expect(user.byKey('email').get()).toBe('hannah@example.com')
|
|
21
|
+
})
|
|
22
|
+
|
|
14
23
|
test('creates stores with initial values', () => {
|
|
15
|
-
// Record store
|
|
16
24
|
const user = createStore({
|
|
17
25
|
name: 'Hannah',
|
|
18
26
|
email: 'hannah@example.com',
|
|
19
27
|
})
|
|
20
28
|
expect(user.name.get()).toBe('Hannah')
|
|
21
29
|
expect(user.email.get()).toBe('hannah@example.com')
|
|
22
|
-
|
|
23
|
-
// Array store
|
|
24
|
-
const numbers = createStore([1, 2, 3])
|
|
25
|
-
expect(numbers[0].get()).toBe(1)
|
|
26
|
-
expect(numbers[1].get()).toBe(2)
|
|
27
|
-
expect(numbers[2].get()).toBe(3)
|
|
28
30
|
})
|
|
29
31
|
|
|
30
32
|
test('has Symbol.toStringTag of Store', () => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expect(recordStore[Symbol.toStringTag]).toBe('Store')
|
|
35
|
-
expect(arrayStore[Symbol.toStringTag]).toBe('Store')
|
|
33
|
+
const store = createStore({ a: 1 })
|
|
34
|
+
expect(store[Symbol.toStringTag]).toBe('Store')
|
|
36
35
|
})
|
|
37
36
|
|
|
38
37
|
test('isStore identifies store instances correctly', () => {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const computed = createComputed(() => 1)
|
|
38
|
+
const store = createStore({ a: 1 })
|
|
39
|
+
const state = new State(1)
|
|
40
|
+
const computed = new Memo(() => 1)
|
|
43
41
|
|
|
44
|
-
expect(isStore(
|
|
45
|
-
expect(isStore(arrayStore)).toBe(true)
|
|
42
|
+
expect(isStore(store)).toBe(true)
|
|
46
43
|
expect(isStore(state)).toBe(false)
|
|
47
44
|
expect(isStore(computed)).toBe(false)
|
|
48
45
|
expect(isStore({})).toBe(false)
|
|
49
|
-
expect(isStore(null)).toBe(false)
|
|
50
46
|
})
|
|
51
47
|
|
|
52
48
|
test('get() returns the complete store value', () => {
|
|
53
|
-
// Record store
|
|
54
49
|
const user = createStore({
|
|
55
|
-
name: '
|
|
56
|
-
email: '
|
|
50
|
+
name: 'Alice',
|
|
51
|
+
email: 'alice@example.com',
|
|
57
52
|
})
|
|
58
53
|
expect(user.get()).toEqual({
|
|
59
|
-
name: '
|
|
60
|
-
email: '
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
// Array store
|
|
64
|
-
const numbers = createStore([1, 2, 3])
|
|
65
|
-
expect(numbers.get()).toEqual([1, 2, 3])
|
|
66
|
-
|
|
67
|
-
// Nested structures
|
|
68
|
-
const participants = createStore([
|
|
69
|
-
{ name: 'Alice', tags: ['admin'] },
|
|
70
|
-
{ name: 'Bob', tags: ['user'] },
|
|
71
|
-
])
|
|
72
|
-
expect(participants[0].name.get()).toBe('Alice')
|
|
73
|
-
expect(participants[0].tags.get()).toEqual(['admin'])
|
|
74
|
-
expect(participants[1].name.get()).toBe('Bob')
|
|
75
|
-
expect(participants[1].tags.get()).toEqual(['user'])
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe('length property and sizing', () => {
|
|
80
|
-
test('length property works for both store types', () => {
|
|
81
|
-
// Record store
|
|
82
|
-
const user = createStore({ name: 'John', age: 25 })
|
|
83
|
-
expect(user.length).toBe(2)
|
|
84
|
-
expect(typeof user.length).toBe('number')
|
|
85
|
-
|
|
86
|
-
// Array store
|
|
87
|
-
const numbers = createStore([1, 2, 3])
|
|
88
|
-
expect(numbers.length).toBe(3)
|
|
89
|
-
expect(typeof numbers.length).toBe('number')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('length is reactive and updates with changes', () => {
|
|
93
|
-
// Record store
|
|
94
|
-
const user = createStore<{ name: string; age?: number }>({
|
|
95
|
-
name: 'John',
|
|
54
|
+
name: 'Alice',
|
|
55
|
+
email: 'alice@example.com',
|
|
96
56
|
})
|
|
97
|
-
expect(user.length).toBe(1)
|
|
98
|
-
user.add('age', 25)
|
|
99
|
-
expect(user.length).toBe(2)
|
|
100
|
-
user.remove('age')
|
|
101
|
-
expect(user.length).toBe(1)
|
|
102
|
-
|
|
103
|
-
// Array store
|
|
104
|
-
const items = createStore([1, 2])
|
|
105
|
-
expect(items.length).toBe(2)
|
|
106
|
-
items.add(3)
|
|
107
|
-
expect(items.length).toBe(3)
|
|
108
|
-
items.remove(1)
|
|
109
|
-
expect(items.length).toBe(2)
|
|
110
57
|
})
|
|
111
58
|
})
|
|
112
59
|
|
|
113
60
|
describe('proxy data access and modification', () => {
|
|
114
61
|
test('properties can be accessed and modified via signals', () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(user.name.get()).toBe('Alice')
|
|
62
|
+
const user = createStore({ name: 'John', age: 30 })
|
|
63
|
+
expect(user.name.get()).toBe('John')
|
|
118
64
|
expect(user.age.get()).toBe(30)
|
|
65
|
+
|
|
119
66
|
user.name.set('Alicia')
|
|
120
67
|
user.age.set(31)
|
|
121
68
|
expect(user.name.get()).toBe('Alicia')
|
|
122
69
|
expect(user.age.get()).toBe(31)
|
|
123
|
-
|
|
124
|
-
// Array store
|
|
125
|
-
const items = createStore(['a', 'b'])
|
|
126
|
-
expect(items[0].get()).toBe('a')
|
|
127
|
-
expect(items[1].get()).toBe('b')
|
|
128
|
-
items[0].set('alpha')
|
|
129
|
-
items[1].set('beta')
|
|
130
|
-
expect(items[0].get()).toBe('alpha')
|
|
131
|
-
expect(items[1].get()).toBe('beta')
|
|
132
70
|
})
|
|
133
71
|
|
|
134
72
|
test('returns undefined for non-existent properties', () => {
|
|
135
|
-
// Record store
|
|
136
73
|
const user = createStore({ name: 'Alice' })
|
|
137
74
|
// @ts-expect-error accessing non-existent property
|
|
138
75
|
expect(user.nonexistent).toBeUndefined()
|
|
139
|
-
|
|
140
|
-
// Array store
|
|
141
|
-
const items = createStore(['a'])
|
|
142
|
-
expect(items[5]).toBeUndefined()
|
|
143
76
|
})
|
|
144
77
|
|
|
145
|
-
test('supports
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
expect(items
|
|
149
|
-
expect(items[1].get()).toBe('one')
|
|
150
|
-
|
|
151
|
-
// Array store with numeric keys
|
|
152
|
-
const numbers = createStore([10, 20])
|
|
153
|
-
expect(numbers[0].get()).toBe(10)
|
|
154
|
-
expect(numbers[1].get()).toBe(20)
|
|
78
|
+
test('supports string key access', () => {
|
|
79
|
+
const items = createStore({ first: 'alpha', second: 'beta' })
|
|
80
|
+
expect(items.first.get()).toBe('alpha')
|
|
81
|
+
expect(items.second.get()).toBe('beta')
|
|
155
82
|
})
|
|
156
83
|
})
|
|
157
84
|
|
|
158
85
|
describe('add() and remove() methods', () => {
|
|
159
|
-
test('add() method
|
|
160
|
-
// Record store - requires key parameter
|
|
86
|
+
test('add() method adds new properties', () => {
|
|
161
87
|
const user = createStore<{ name: string; email?: string }>({
|
|
162
88
|
name: 'John',
|
|
163
89
|
})
|
|
164
90
|
user.add('email', 'john@example.com')
|
|
91
|
+
expect(user.byKey('email')?.get()).toBe('john@example.com')
|
|
165
92
|
expect(user.email?.get()).toBe('john@example.com')
|
|
166
|
-
expect(user.length).toBe(2)
|
|
167
|
-
|
|
168
|
-
// Array store - single parameter adds to end
|
|
169
|
-
const fruits = createStore(['apple', 'banana'])
|
|
170
|
-
fruits.add('cherry')
|
|
171
|
-
expect(fruits[2].get()).toBe('cherry')
|
|
172
|
-
expect(fruits.length).toBe(3)
|
|
173
|
-
expect(fruits.get()).toEqual(['apple', 'banana', 'cherry'])
|
|
174
93
|
})
|
|
175
94
|
|
|
176
|
-
test('remove() method
|
|
177
|
-
|
|
178
|
-
const user = createStore({
|
|
95
|
+
test('remove() method removes properties', () => {
|
|
96
|
+
const user = createStore<{ name: string; email?: string }>({
|
|
179
97
|
name: 'John',
|
|
180
98
|
email: 'john@example.com',
|
|
181
99
|
})
|
|
182
100
|
user.remove('email')
|
|
101
|
+
expect(user.byKey('email')).toBeUndefined()
|
|
102
|
+
// expect(user.byKey('name').get()).toBe('John')
|
|
183
103
|
expect(user.email).toBeUndefined()
|
|
184
|
-
expect(user.name.get()).toBe('John')
|
|
185
|
-
expect(user.length).toBe(1)
|
|
186
|
-
|
|
187
|
-
// Array store - removes by index
|
|
188
|
-
const items = createStore(['a', 'b', 'c'])
|
|
189
|
-
items.remove(1) // Remove 'b'
|
|
190
|
-
expect(items.get()).toEqual(['a', 'c'])
|
|
191
|
-
expect(items.length).toBe(2)
|
|
104
|
+
// expect(user.name.get()).toBe('John')
|
|
192
105
|
})
|
|
193
106
|
|
|
194
|
-
test('add method prevents null values
|
|
195
|
-
// Record store
|
|
107
|
+
test('add method prevents null values', () => {
|
|
196
108
|
const user = createStore<{ name: string; email?: string }>({
|
|
197
109
|
name: 'John',
|
|
198
110
|
})
|
|
199
111
|
// @ts-expect-error testing null values
|
|
200
112
|
expect(() => user.add('email', null)).toThrow()
|
|
201
|
-
|
|
202
|
-
// Array store
|
|
203
|
-
const items = createStore([1])
|
|
204
|
-
// @ts-expect-error testing null values
|
|
205
|
-
expect(() => items.add(null)).toThrow()
|
|
206
113
|
})
|
|
207
114
|
|
|
208
|
-
test('add method prevents overwriting existing properties
|
|
209
|
-
const user = createStore
|
|
115
|
+
test('add method prevents overwriting existing properties', () => {
|
|
116
|
+
const user = createStore({
|
|
210
117
|
name: 'John',
|
|
211
118
|
email: 'john@example.com',
|
|
212
119
|
})
|
|
213
|
-
const originalSize = user.length
|
|
214
120
|
expect(() => user.add('name', 'Jane')).toThrow()
|
|
215
|
-
expect(user.length).toBe(originalSize)
|
|
216
|
-
expect(user.name.get()).toBe('John')
|
|
217
121
|
})
|
|
218
122
|
|
|
219
123
|
test('remove method handles non-existent properties gracefully', () => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const originalSize = user.length
|
|
223
|
-
// @ts-expect-error deliberate removal of non-existent property
|
|
224
|
-
user.remove('nonexistent')
|
|
225
|
-
expect(user.length).toBe(originalSize)
|
|
226
|
-
|
|
227
|
-
// Array store - out of bounds throws
|
|
228
|
-
const items = createStore([1, 2])
|
|
229
|
-
expect(() => items.remove(5)).toThrow()
|
|
230
|
-
expect(() => items.remove(-5)).toThrow()
|
|
124
|
+
const user = createStore({ name: 'John' })
|
|
125
|
+
expect(() => user.remove('nonexistent')).not.toThrow()
|
|
231
126
|
})
|
|
232
127
|
})
|
|
233
128
|
|
|
234
129
|
describe('nested stores', () => {
|
|
235
|
-
test('creates nested stores for object properties
|
|
236
|
-
// Record store
|
|
130
|
+
test('creates nested stores for object properties', () => {
|
|
237
131
|
const user = createStore({
|
|
238
132
|
name: 'Alice',
|
|
239
133
|
preferences: {
|
|
240
|
-
theme: '
|
|
134
|
+
theme: 'light',
|
|
241
135
|
notifications: true,
|
|
242
136
|
},
|
|
243
137
|
})
|
|
244
|
-
expect(isStore(user.preferences)).toBe(true)
|
|
245
|
-
expect(user.preferences.theme.get()).toBe('dark')
|
|
246
|
-
expect(user.preferences.notifications.get()).toBe(true)
|
|
247
138
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
{ name: 'Bob', active: false },
|
|
252
|
-
])
|
|
253
|
-
expect(isStore(users[0])).toBe(true)
|
|
254
|
-
expect(users[0].name.get()).toBe('Alice')
|
|
255
|
-
expect(users[1].active.get()).toBe(false)
|
|
139
|
+
expect(user.name.get()).toBe('Alice')
|
|
140
|
+
expect(user.preferences.theme.get()).toBe('light')
|
|
141
|
+
expect(user.preferences.notifications.get()).toBe(true)
|
|
256
142
|
})
|
|
257
143
|
|
|
258
144
|
test('nested properties are reactive', () => {
|
|
259
|
-
// Record store
|
|
260
145
|
const user = createStore({
|
|
261
146
|
preferences: {
|
|
262
147
|
theme: 'light',
|
|
@@ -266,19 +151,10 @@ describe('store', () => {
|
|
|
266
151
|
createEffect(() => {
|
|
267
152
|
lastTheme = user.preferences.theme.get()
|
|
268
153
|
})
|
|
154
|
+
|
|
269
155
|
expect(lastTheme).toBe('light')
|
|
270
156
|
user.preferences.theme.set('dark')
|
|
271
157
|
expect(lastTheme).toBe('dark')
|
|
272
|
-
|
|
273
|
-
// Array store
|
|
274
|
-
const configs = createStore([{ mode: 'development' }])
|
|
275
|
-
let lastMode = ''
|
|
276
|
-
createEffect(() => {
|
|
277
|
-
lastMode = configs[0].mode.get()
|
|
278
|
-
})
|
|
279
|
-
expect(lastMode).toBe('development')
|
|
280
|
-
configs[0].mode.set('production')
|
|
281
|
-
expect(lastMode).toBe('production')
|
|
282
158
|
})
|
|
283
159
|
|
|
284
160
|
test('deeply nested stores work correctly', () => {
|
|
@@ -286,20 +162,20 @@ describe('store', () => {
|
|
|
286
162
|
ui: {
|
|
287
163
|
theme: {
|
|
288
164
|
colors: {
|
|
289
|
-
primary: '#
|
|
165
|
+
primary: '#007acc',
|
|
290
166
|
},
|
|
291
167
|
},
|
|
292
168
|
},
|
|
293
169
|
})
|
|
294
|
-
|
|
295
|
-
config.ui.theme.colors.primary.
|
|
296
|
-
|
|
170
|
+
|
|
171
|
+
expect(config.ui.theme.colors.primary.get()).toBe('#007acc')
|
|
172
|
+
config.ui.theme.colors.primary.set('#ff6600')
|
|
173
|
+
expect(config.ui.theme.colors.primary.get()).toBe('#ff6600')
|
|
297
174
|
})
|
|
298
175
|
})
|
|
299
176
|
|
|
300
177
|
describe('set() and update() methods', () => {
|
|
301
|
-
test('set() replaces entire store value
|
|
302
|
-
// Record store
|
|
178
|
+
test('set() replaces entire store value', () => {
|
|
303
179
|
const user = createStore({
|
|
304
180
|
name: 'John',
|
|
305
181
|
email: 'john@example.com',
|
|
@@ -307,31 +183,18 @@ describe('store', () => {
|
|
|
307
183
|
user.set({ name: 'Jane', email: 'jane@example.com' })
|
|
308
184
|
expect(user.name.get()).toBe('Jane')
|
|
309
185
|
expect(user.email.get()).toBe('jane@example.com')
|
|
310
|
-
|
|
311
|
-
// Array store
|
|
312
|
-
const numbers = createStore([1, 2, 3])
|
|
313
|
-
numbers.set([4, 5])
|
|
314
|
-
expect(numbers.get()).toEqual([4, 5])
|
|
315
|
-
expect(numbers.length).toBe(2)
|
|
316
186
|
})
|
|
317
187
|
|
|
318
|
-
test('update() modifies store using function
|
|
319
|
-
// Record store
|
|
188
|
+
test('update() modifies store using function', () => {
|
|
320
189
|
const user = createStore({ name: 'John', age: 25 })
|
|
321
190
|
user.update(u => ({ ...u, age: u.age + 1 }))
|
|
322
191
|
expect(user.name.get()).toBe('John')
|
|
323
192
|
expect(user.age.get()).toBe(26)
|
|
324
|
-
|
|
325
|
-
// Array store
|
|
326
|
-
const numbers = createStore([1, 2, 3])
|
|
327
|
-
numbers.update(arr => arr.map(n => n * 2))
|
|
328
|
-
expect(numbers.get()).toEqual([2, 4, 6])
|
|
329
193
|
})
|
|
330
194
|
})
|
|
331
195
|
|
|
332
196
|
describe('iteration protocol', () => {
|
|
333
|
-
test('supports for...of iteration
|
|
334
|
-
// Record store - yields [key, signal] pairs
|
|
197
|
+
test('supports for...of iteration', () => {
|
|
335
198
|
const user = createStore({ name: 'John', age: 25 })
|
|
336
199
|
const entries = [...user]
|
|
337
200
|
expect(entries).toHaveLength(2)
|
|
@@ -339,193 +202,119 @@ describe('store', () => {
|
|
|
339
202
|
expect(entries[0][1].get()).toBe('John')
|
|
340
203
|
expect(entries[1][0]).toBe('age')
|
|
341
204
|
expect(entries[1][1].get()).toBe(25)
|
|
342
|
-
|
|
343
|
-
// Array store - yields signals only
|
|
344
|
-
const numbers = createStore([10, 20, 30])
|
|
345
|
-
const signals = [...numbers]
|
|
346
|
-
expect(signals).toHaveLength(3)
|
|
347
|
-
expect(signals[0].get()).toBe(10)
|
|
348
|
-
expect(signals[1].get()).toBe(20)
|
|
349
|
-
expect(signals[2].get()).toBe(30)
|
|
350
205
|
})
|
|
351
206
|
|
|
352
|
-
test('Symbol.isConcatSpreadable
|
|
353
|
-
// Array store - spreadable
|
|
354
|
-
const numbers = createStore([1, 2, 3])
|
|
355
|
-
expect(numbers[Symbol.isConcatSpreadable]).toBe(true)
|
|
356
|
-
|
|
357
|
-
// Record store - not spreadable
|
|
207
|
+
test('Symbol.isConcatSpreadable is false', () => {
|
|
358
208
|
const user = createStore({ name: 'John', age: 25 })
|
|
359
209
|
expect(user[Symbol.isConcatSpreadable]).toBe(false)
|
|
360
210
|
})
|
|
361
211
|
|
|
362
|
-
test('
|
|
363
|
-
const
|
|
364
|
-
const keys = Object.keys(
|
|
365
|
-
|
|
212
|
+
test('maintains property key ordering', () => {
|
|
213
|
+
const config = createStore({ alpha: 1, beta: 2, gamma: 3 })
|
|
214
|
+
const keys = Object.keys(config)
|
|
215
|
+
expect(keys).toEqual(['alpha', 'beta', 'gamma'])
|
|
216
|
+
|
|
217
|
+
const entries = [...config]
|
|
218
|
+
expect(entries.map(([key, signal]) => [key, signal.get()])).toEqual(
|
|
219
|
+
[
|
|
220
|
+
['alpha', 1],
|
|
221
|
+
['beta', 2],
|
|
222
|
+
['gamma', 3],
|
|
223
|
+
],
|
|
366
224
|
)
|
|
367
|
-
expect(keys).toEqual(['0', '1', '2'])
|
|
368
|
-
|
|
369
|
-
const signals = [...items]
|
|
370
|
-
expect(signals.map(s => s.get())).toEqual([
|
|
371
|
-
'first',
|
|
372
|
-
'second',
|
|
373
|
-
'third',
|
|
374
|
-
])
|
|
375
225
|
})
|
|
376
226
|
})
|
|
377
227
|
|
|
378
228
|
describe('change tracking and notifications', () => {
|
|
379
|
-
test('emits add notifications
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
user.on('add', change => {
|
|
384
|
-
addNotification = change
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
// Wait for initial add event
|
|
388
|
-
setTimeout(() => {
|
|
389
|
-
expect(addNotification.name).toBe('John')
|
|
390
|
-
}, 0)
|
|
391
|
-
|
|
392
|
-
// Record store - new property
|
|
393
|
-
const userWithEmail = createStore<{ name: string; email?: string }>(
|
|
394
|
-
{ name: 'John' },
|
|
395
|
-
)
|
|
396
|
-
let newAddNotification: Record<string, unknown> = {}
|
|
397
|
-
userWithEmail.on('add', change => {
|
|
398
|
-
newAddNotification = change
|
|
229
|
+
test('emits add notifications', () => {
|
|
230
|
+
let addNotification: readonly string[] = []
|
|
231
|
+
const user = createStore<{ name: string; email?: string }>({
|
|
232
|
+
name: 'John',
|
|
399
233
|
})
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
// Array store
|
|
404
|
-
const numbers = createStore([1, 2])
|
|
405
|
-
let arrayAddNotification = {}
|
|
406
|
-
numbers.on('add', change => {
|
|
407
|
-
arrayAddNotification = change
|
|
234
|
+
user.on('add', add => {
|
|
235
|
+
addNotification = add
|
|
408
236
|
})
|
|
409
|
-
|
|
410
|
-
expect(
|
|
237
|
+
user.add('email', 'john@example.com')
|
|
238
|
+
expect(addNotification).toContain('email')
|
|
411
239
|
})
|
|
412
240
|
|
|
413
241
|
test('emits change notifications when properties are modified', () => {
|
|
414
|
-
// Record store
|
|
415
242
|
const user = createStore({ name: 'John' })
|
|
416
|
-
let changeNotification:
|
|
243
|
+
let changeNotification: readonly string[] = []
|
|
417
244
|
user.on('change', change => {
|
|
418
245
|
changeNotification = change
|
|
419
246
|
})
|
|
420
247
|
user.name.set('Jane')
|
|
421
|
-
expect(changeNotification
|
|
422
|
-
|
|
423
|
-
// Array store
|
|
424
|
-
const items = createStore(['a', 'b'])
|
|
425
|
-
let arrayChangeNotification = {}
|
|
426
|
-
items.on('change', change => {
|
|
427
|
-
arrayChangeNotification = change
|
|
428
|
-
})
|
|
429
|
-
items[0].set('alpha')
|
|
430
|
-
expect(arrayChangeNotification[0]).toBe('alpha')
|
|
248
|
+
expect(changeNotification).toContain('name')
|
|
431
249
|
})
|
|
432
250
|
|
|
433
251
|
test('emits change notifications for nested property changes', () => {
|
|
434
|
-
// Record store
|
|
435
252
|
const user = createStore({
|
|
436
|
-
name: 'John',
|
|
437
253
|
preferences: {
|
|
438
254
|
theme: 'light',
|
|
439
|
-
notifications: true,
|
|
440
255
|
},
|
|
441
256
|
})
|
|
442
|
-
let changeNotification:
|
|
257
|
+
let changeNotification: readonly string[] = []
|
|
443
258
|
user.on('change', change => {
|
|
444
259
|
changeNotification = change
|
|
445
260
|
})
|
|
446
261
|
user.preferences.theme.set('dark')
|
|
447
|
-
expect(changeNotification.preferences)
|
|
448
|
-
theme: 'dark',
|
|
449
|
-
notifications: true,
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
// Array store with nested objects
|
|
453
|
-
const users = createStore([{ name: 'Alice', role: 'admin' }])
|
|
454
|
-
let arrayChangeNotification: Record<number, unknown> = []
|
|
455
|
-
users.on('change', change => {
|
|
456
|
-
arrayChangeNotification = change
|
|
457
|
-
})
|
|
458
|
-
users[0].name.set('Alicia')
|
|
459
|
-
expect(arrayChangeNotification[0]).toEqual({
|
|
460
|
-
name: 'Alicia',
|
|
461
|
-
role: 'admin',
|
|
462
|
-
})
|
|
262
|
+
expect(changeNotification).toContain('preferences')
|
|
463
263
|
})
|
|
464
264
|
|
|
465
265
|
test('emits remove notifications when properties are removed', () => {
|
|
466
|
-
// Record store
|
|
467
266
|
const user = createStore({
|
|
468
267
|
name: 'John',
|
|
469
268
|
email: 'john@example.com',
|
|
470
269
|
})
|
|
471
|
-
let removeNotification:
|
|
472
|
-
user.on('remove',
|
|
473
|
-
removeNotification =
|
|
270
|
+
let removeNotification: readonly string[] = []
|
|
271
|
+
user.on('remove', remove => {
|
|
272
|
+
removeNotification = remove
|
|
474
273
|
})
|
|
475
274
|
user.remove('email')
|
|
476
|
-
expect(removeNotification
|
|
477
|
-
|
|
478
|
-
// Array store
|
|
479
|
-
const items = createStore(['a', 'b', 'c'])
|
|
480
|
-
let arrayRemoveNotification: Record<number, unknown> = []
|
|
481
|
-
items.on('remove', change => {
|
|
482
|
-
arrayRemoveNotification = change
|
|
483
|
-
})
|
|
484
|
-
items.remove(1)
|
|
485
|
-
expect(arrayRemoveNotification[2]).toBe(UNSET) // Last item gets removed in compaction
|
|
275
|
+
expect(removeNotification).toContain('email')
|
|
486
276
|
})
|
|
487
277
|
|
|
488
278
|
test('set() correctly handles mixed changes, additions, and removals', () => {
|
|
489
279
|
const user = createStore<{
|
|
490
280
|
name: string
|
|
491
281
|
email?: string
|
|
492
|
-
preferences?:
|
|
493
|
-
theme: string
|
|
494
|
-
notifications?: boolean
|
|
495
|
-
}
|
|
282
|
+
preferences: { theme?: string }
|
|
496
283
|
age?: number
|
|
497
284
|
}>({
|
|
498
|
-
name: '
|
|
499
|
-
email: '
|
|
285
|
+
name: 'John',
|
|
286
|
+
email: 'john@example.com',
|
|
500
287
|
preferences: {
|
|
501
|
-
theme: 'light',
|
|
288
|
+
theme: 'light',
|
|
502
289
|
},
|
|
503
290
|
})
|
|
504
291
|
|
|
505
|
-
let changeNotification:
|
|
506
|
-
let addNotification:
|
|
507
|
-
let removeNotification:
|
|
292
|
+
let changeNotification: readonly string[] = []
|
|
293
|
+
let addNotification: readonly string[] = []
|
|
294
|
+
let removeNotification: readonly string[] = []
|
|
295
|
+
|
|
508
296
|
user.on('change', change => {
|
|
509
297
|
changeNotification = change
|
|
510
298
|
})
|
|
511
|
-
user.on('add',
|
|
512
|
-
addNotification =
|
|
299
|
+
user.on('add', add => {
|
|
300
|
+
addNotification = add
|
|
513
301
|
})
|
|
514
|
-
user.on('remove',
|
|
515
|
-
removeNotification =
|
|
302
|
+
user.on('remove', remove => {
|
|
303
|
+
removeNotification = remove
|
|
516
304
|
})
|
|
517
305
|
|
|
518
306
|
user.set({
|
|
519
|
-
name: 'Jane',
|
|
307
|
+
name: 'Jane',
|
|
520
308
|
preferences: {
|
|
521
|
-
theme: 'dark',
|
|
309
|
+
theme: 'dark',
|
|
522
310
|
},
|
|
523
|
-
age: 30,
|
|
524
|
-
}
|
|
311
|
+
age: 30,
|
|
312
|
+
})
|
|
525
313
|
|
|
526
|
-
expect(changeNotification
|
|
527
|
-
expect(
|
|
528
|
-
expect(
|
|
314
|
+
expect(changeNotification).toContain('name')
|
|
315
|
+
expect(changeNotification).toContain('preferences')
|
|
316
|
+
expect(addNotification).toContain('age')
|
|
317
|
+
expect(removeNotification).toContain('email')
|
|
529
318
|
})
|
|
530
319
|
|
|
531
320
|
test('notification listeners can be removed', () => {
|
|
@@ -537,15 +326,15 @@ describe('store', () => {
|
|
|
537
326
|
const off = user.on('change', listener)
|
|
538
327
|
user.name.set('Jane')
|
|
539
328
|
expect(notificationCount).toBe(1)
|
|
329
|
+
|
|
540
330
|
off()
|
|
541
|
-
user.name.set('
|
|
542
|
-
expect(notificationCount).toBe(1)
|
|
331
|
+
user.name.set('Bob')
|
|
332
|
+
expect(notificationCount).toBe(1)
|
|
543
333
|
})
|
|
544
334
|
})
|
|
545
335
|
|
|
546
336
|
describe('reactivity', () => {
|
|
547
|
-
test('store-level get() is reactive
|
|
548
|
-
// Record store
|
|
337
|
+
test('store-level get() is reactive', () => {
|
|
549
338
|
const user = createStore({
|
|
550
339
|
name: 'John',
|
|
551
340
|
email: 'john@example.com',
|
|
@@ -554,27 +343,22 @@ describe('store', () => {
|
|
|
554
343
|
createEffect(() => {
|
|
555
344
|
lastValue = user.get()
|
|
556
345
|
})
|
|
346
|
+
|
|
557
347
|
expect(lastValue).toEqual({
|
|
558
348
|
name: 'John',
|
|
559
349
|
email: 'john@example.com',
|
|
560
350
|
})
|
|
351
|
+
|
|
561
352
|
user.name.set('Jane')
|
|
562
|
-
|
|
563
|
-
expect(lastValue.email).toBe('john@example.com')
|
|
353
|
+
user.email.set('jane@example.com')
|
|
564
354
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
createEffect(() => {
|
|
569
|
-
lastArray = numbers.get()
|
|
355
|
+
expect(lastValue).toEqual({
|
|
356
|
+
name: 'Jane',
|
|
357
|
+
email: 'jane@example.com',
|
|
570
358
|
})
|
|
571
|
-
expect(lastArray).toEqual([1, 2, 3])
|
|
572
|
-
numbers[0].set(10)
|
|
573
|
-
expect(lastArray).toEqual([10, 2, 3])
|
|
574
359
|
})
|
|
575
360
|
|
|
576
|
-
test('individual signal reactivity works
|
|
577
|
-
// Record store
|
|
361
|
+
test('individual signal reactivity works', () => {
|
|
578
362
|
const user = createStore({
|
|
579
363
|
name: 'John',
|
|
580
364
|
email: 'john@example.com',
|
|
@@ -582,34 +366,16 @@ describe('store', () => {
|
|
|
582
366
|
let lastName = ''
|
|
583
367
|
let nameEffectRuns = 0
|
|
584
368
|
createEffect(() => {
|
|
585
|
-
nameEffectRuns++
|
|
586
369
|
lastName = user.name.get()
|
|
370
|
+
nameEffectRuns++
|
|
587
371
|
})
|
|
372
|
+
|
|
588
373
|
expect(lastName).toBe('John')
|
|
589
374
|
expect(nameEffectRuns).toBe(1)
|
|
375
|
+
|
|
590
376
|
user.name.set('Jane')
|
|
591
377
|
expect(lastName).toBe('Jane')
|
|
592
378
|
expect(nameEffectRuns).toBe(2)
|
|
593
|
-
// Changing email should not trigger name effect
|
|
594
|
-
user.email.set('jane@example.com')
|
|
595
|
-
expect(nameEffectRuns).toBe(2)
|
|
596
|
-
|
|
597
|
-
// Array store
|
|
598
|
-
const items = createStore(['a', 'b'])
|
|
599
|
-
let lastItem = ''
|
|
600
|
-
let itemEffectRuns = 0
|
|
601
|
-
createEffect(() => {
|
|
602
|
-
itemEffectRuns++
|
|
603
|
-
lastItem = items[0].get()
|
|
604
|
-
})
|
|
605
|
-
expect(lastItem).toBe('a')
|
|
606
|
-
expect(itemEffectRuns).toBe(1)
|
|
607
|
-
items[0].set('alpha')
|
|
608
|
-
expect(lastItem).toBe('alpha')
|
|
609
|
-
expect(itemEffectRuns).toBe(2)
|
|
610
|
-
// Changing other item should not trigger effect
|
|
611
|
-
items[1].set('beta')
|
|
612
|
-
expect(itemEffectRuns).toBe(2)
|
|
613
379
|
})
|
|
614
380
|
|
|
615
381
|
test('nested store changes propagate to parent', () => {
|
|
@@ -620,101 +386,83 @@ describe('store', () => {
|
|
|
620
386
|
})
|
|
621
387
|
let effectRuns = 0
|
|
622
388
|
createEffect(() => {
|
|
389
|
+
user.get()
|
|
623
390
|
effectRuns++
|
|
624
|
-
user.get() // Subscribe to entire store
|
|
625
391
|
})
|
|
392
|
+
|
|
626
393
|
expect(effectRuns).toBe(1)
|
|
627
394
|
user.preferences.theme.set('dark')
|
|
628
395
|
expect(effectRuns).toBe(2)
|
|
629
396
|
})
|
|
630
397
|
|
|
631
|
-
test('updates are reactive
|
|
632
|
-
|
|
633
|
-
const user = createStore<{ name: string; email?: string }>({
|
|
398
|
+
test('updates are reactive', () => {
|
|
399
|
+
const user = createStore({
|
|
634
400
|
name: 'John',
|
|
635
401
|
})
|
|
636
|
-
let lastValue:
|
|
402
|
+
let lastValue: {
|
|
403
|
+
name: string
|
|
404
|
+
email?: string
|
|
405
|
+
} = { name: '' }
|
|
637
406
|
let effectRuns = 0
|
|
638
407
|
createEffect(() => {
|
|
639
|
-
effectRuns++
|
|
640
408
|
lastValue = user.get()
|
|
409
|
+
effectRuns++
|
|
641
410
|
})
|
|
411
|
+
|
|
412
|
+
expect(lastValue).toEqual({ name: 'John' })
|
|
642
413
|
expect(effectRuns).toBe(1)
|
|
643
|
-
user.update(u => ({ ...u, email: 'john@example.com' }))
|
|
644
|
-
expect(effectRuns).toBe(2)
|
|
645
|
-
expect(lastValue.name).toBe('John')
|
|
646
|
-
expect(lastValue.email).toBe('john@example.com')
|
|
647
414
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
createEffect(() => {
|
|
653
|
-
arrayEffectRuns++
|
|
654
|
-
lastArray = numbers.get()
|
|
415
|
+
user.update(u => ({ ...u, email: 'john@example.com' }))
|
|
416
|
+
expect(lastValue).toEqual({
|
|
417
|
+
name: 'John',
|
|
418
|
+
email: 'john@example.com',
|
|
655
419
|
})
|
|
656
|
-
expect(
|
|
657
|
-
numbers.update(arr => [...arr, 3])
|
|
658
|
-
expect(arrayEffectRuns).toBe(2)
|
|
659
|
-
expect(lastArray).toEqual([1, 2, 3])
|
|
420
|
+
expect(effectRuns).toBe(2)
|
|
660
421
|
})
|
|
661
422
|
|
|
662
|
-
test('remove method is reactive
|
|
663
|
-
|
|
664
|
-
const user = createStore<{
|
|
665
|
-
name: string
|
|
666
|
-
email?: string
|
|
667
|
-
}>({
|
|
423
|
+
test('remove method is reactive', () => {
|
|
424
|
+
const user = createStore({
|
|
668
425
|
name: 'John',
|
|
669
426
|
email: 'john@example.com',
|
|
427
|
+
age: 30,
|
|
670
428
|
})
|
|
671
|
-
let lastValue:
|
|
429
|
+
let lastValue: {
|
|
430
|
+
name: string
|
|
431
|
+
email?: string
|
|
432
|
+
age?: number
|
|
433
|
+
} = { name: '', email: '', age: 0 }
|
|
672
434
|
let effectRuns = 0
|
|
673
435
|
createEffect(() => {
|
|
674
|
-
effectRuns++
|
|
675
436
|
lastValue = user.get()
|
|
437
|
+
effectRuns++
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
expect(lastValue).toEqual({
|
|
441
|
+
name: 'John',
|
|
442
|
+
email: 'john@example.com',
|
|
443
|
+
age: 30,
|
|
676
444
|
})
|
|
677
445
|
expect(effectRuns).toBe(1)
|
|
446
|
+
|
|
678
447
|
user.remove('email')
|
|
448
|
+
expect(lastValue).toEqual({ name: 'John', age: 30 })
|
|
679
449
|
expect(effectRuns).toBe(2)
|
|
680
|
-
expect(lastValue.name).toBe('John')
|
|
681
|
-
expect(lastValue.email).toBeUndefined()
|
|
682
|
-
|
|
683
|
-
// Array store
|
|
684
|
-
const items = createStore(['a', 'b', 'c'])
|
|
685
|
-
let lastArray: string[] = []
|
|
686
|
-
let arrayEffectRuns = 0
|
|
687
|
-
createEffect(() => {
|
|
688
|
-
arrayEffectRuns++
|
|
689
|
-
lastArray = items.get()
|
|
690
|
-
})
|
|
691
|
-
expect(arrayEffectRuns).toBe(1)
|
|
692
|
-
items.remove(1)
|
|
693
|
-
// Array removal causes multiple reactivity updates due to compaction
|
|
694
|
-
expect(arrayEffectRuns).toBeGreaterThanOrEqual(2)
|
|
695
|
-
expect(lastArray).toEqual(['a', 'c'])
|
|
696
450
|
})
|
|
697
451
|
})
|
|
698
452
|
|
|
699
453
|
describe('computed integration', () => {
|
|
700
|
-
test('works with computed signals
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return `${user.firstName.get()} ${user.lastName.get()}`
|
|
454
|
+
test('works with computed signals', () => {
|
|
455
|
+
const user = createStore({
|
|
456
|
+
firstName: 'John',
|
|
457
|
+
lastName: 'Doe',
|
|
705
458
|
})
|
|
459
|
+
const fullName = new Memo(
|
|
460
|
+
() => `${user.firstName.get()} ${user.lastName.get()}`,
|
|
461
|
+
)
|
|
462
|
+
|
|
706
463
|
expect(fullName.get()).toBe('John Doe')
|
|
707
464
|
user.firstName.set('Jane')
|
|
708
465
|
expect(fullName.get()).toBe('Jane Doe')
|
|
709
|
-
|
|
710
|
-
// Array store
|
|
711
|
-
const numbers = createStore([1, 2, 3])
|
|
712
|
-
const sum = createComputed(() => {
|
|
713
|
-
return numbers.get().reduce((acc, n) => acc + n, 0)
|
|
714
|
-
})
|
|
715
|
-
expect(sum.get()).toBe(6)
|
|
716
|
-
numbers[0].set(10)
|
|
717
|
-
expect(sum.get()).toBe(15)
|
|
718
466
|
})
|
|
719
467
|
|
|
720
468
|
test('computed reacts to nested store changes', () => {
|
|
@@ -723,226 +471,46 @@ describe('store', () => {
|
|
|
723
471
|
theme: 'light',
|
|
724
472
|
},
|
|
725
473
|
})
|
|
726
|
-
const themeDisplay =
|
|
727
|
-
|
|
728
|
-
|
|
474
|
+
const themeDisplay = new Memo(
|
|
475
|
+
() => `Theme: ${config.ui.theme.get()}`,
|
|
476
|
+
)
|
|
477
|
+
|
|
729
478
|
expect(themeDisplay.get()).toBe('Theme: light')
|
|
730
479
|
config.ui.theme.set('dark')
|
|
731
480
|
expect(themeDisplay.get()).toBe('Theme: dark')
|
|
732
481
|
})
|
|
733
|
-
|
|
734
|
-
test('computed with array stores handles additions and removals', () => {
|
|
735
|
-
const numbers = createStore([1, 2, 3])
|
|
736
|
-
const sum = createComputed(() => {
|
|
737
|
-
const array = numbers.get()
|
|
738
|
-
return array.reduce((acc, n) => acc + n, 0)
|
|
739
|
-
})
|
|
740
|
-
|
|
741
|
-
expect(sum.get()).toBe(6)
|
|
742
|
-
|
|
743
|
-
// Add a number
|
|
744
|
-
numbers.add(4)
|
|
745
|
-
expect(sum.get()).toBe(10)
|
|
746
|
-
|
|
747
|
-
// Remove a number
|
|
748
|
-
numbers.remove(0)
|
|
749
|
-
const finalArray = numbers.get()
|
|
750
|
-
expect(finalArray).toEqual([2, 3, 4])
|
|
751
|
-
expect(sum.get()).toBe(9)
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
test('computed sum using store iteration with length tracking', () => {
|
|
755
|
-
const numbers = createStore([1, 2, 3])
|
|
756
|
-
|
|
757
|
-
const sum = createComputed(() => {
|
|
758
|
-
// Access length to ensure reactivity
|
|
759
|
-
const _length = numbers.length
|
|
760
|
-
let total = 0
|
|
761
|
-
for (const signal of numbers) {
|
|
762
|
-
total += signal.get()
|
|
763
|
-
}
|
|
764
|
-
return total
|
|
765
|
-
})
|
|
766
|
-
|
|
767
|
-
expect(sum.get()).toBe(6)
|
|
768
|
-
|
|
769
|
-
// Add item
|
|
770
|
-
numbers.add(4)
|
|
771
|
-
expect(sum.get()).toBe(10)
|
|
772
|
-
|
|
773
|
-
// Remove item
|
|
774
|
-
numbers.remove(1)
|
|
775
|
-
expect(sum.get()).toBe(8) // 1 + 3 + 4 (middle item removed)
|
|
776
|
-
})
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
describe('sort() method', () => {
|
|
780
|
-
test('sorts array stores with different compare functions', () => {
|
|
781
|
-
// Numeric sort
|
|
782
|
-
const numbers = createStore([3, 1, 4, 1, 5])
|
|
783
|
-
const _oldSignals = [
|
|
784
|
-
numbers[0],
|
|
785
|
-
numbers[1],
|
|
786
|
-
numbers[2],
|
|
787
|
-
numbers[3],
|
|
788
|
-
numbers[4],
|
|
789
|
-
]
|
|
790
|
-
|
|
791
|
-
numbers.sort((a, b) => a - b)
|
|
792
|
-
expect(numbers.get()).toEqual([1, 1, 3, 4, 5])
|
|
793
|
-
|
|
794
|
-
// Verify signals moved correctly
|
|
795
|
-
expect(numbers[0]).toBe(_oldSignals[1]) // First '1'
|
|
796
|
-
expect(numbers[1]).toBe(_oldSignals[3]) // Second '1'
|
|
797
|
-
expect(numbers[2]).toBe(_oldSignals[0]) // '3'
|
|
798
|
-
|
|
799
|
-
// String sort
|
|
800
|
-
const names = createStore(['Charlie', 'Alice', 'Bob'])
|
|
801
|
-
names.sort()
|
|
802
|
-
expect(names.get()).toEqual(['Alice', 'Bob', 'Charlie'])
|
|
803
|
-
})
|
|
804
|
-
|
|
805
|
-
test('sorts record stores by value', () => {
|
|
806
|
-
const users = createStore({
|
|
807
|
-
user1: { name: 'Charlie', age: 25 },
|
|
808
|
-
user2: { name: 'Alice', age: 30 },
|
|
809
|
-
user3: { name: 'Bob', age: 35 },
|
|
810
|
-
})
|
|
811
|
-
|
|
812
|
-
const _oldSignals = {
|
|
813
|
-
user1: users.user1,
|
|
814
|
-
user2: users.user2,
|
|
815
|
-
user3: users.user3,
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
users.sort((a, b) => a.name.localeCompare(b.name))
|
|
819
|
-
|
|
820
|
-
// After sorting by name: Alice, Bob, Charlie
|
|
821
|
-
// The keys should be reordered based on the sort
|
|
822
|
-
const sortedEntries = [...users]
|
|
823
|
-
expect(sortedEntries[0][1].name.get()).toBe('Alice')
|
|
824
|
-
expect(sortedEntries[1][1].name.get()).toBe('Bob')
|
|
825
|
-
expect(sortedEntries[2][1].name.get()).toBe('Charlie')
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
test('emits sort notification with new order', () => {
|
|
829
|
-
const numbers = createStore([3, 1, 2])
|
|
830
|
-
let sortNotification: string[] = []
|
|
831
|
-
numbers.on('sort', change => {
|
|
832
|
-
sortNotification = change
|
|
833
|
-
})
|
|
834
|
-
|
|
835
|
-
numbers.sort((a, b) => a - b)
|
|
836
|
-
expect(sortNotification).toEqual(['1', '2', '0']) // Original indices in new order
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
test('sort is reactive - watchers are notified', () => {
|
|
840
|
-
const numbers = createStore([3, 1, 2])
|
|
841
|
-
let effectCount = 0
|
|
842
|
-
let lastValue: number[] = []
|
|
843
|
-
|
|
844
|
-
createEffect(() => {
|
|
845
|
-
effectCount++
|
|
846
|
-
lastValue = numbers.get()
|
|
847
|
-
})
|
|
848
|
-
|
|
849
|
-
expect(effectCount).toBe(1)
|
|
850
|
-
expect(lastValue).toEqual([3, 1, 2])
|
|
851
|
-
|
|
852
|
-
numbers.sort((a, b) => a - b)
|
|
853
|
-
expect(effectCount).toBe(2)
|
|
854
|
-
expect(lastValue).toEqual([1, 2, 3])
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
test('nested signals remain reactive after sorting', () => {
|
|
858
|
-
const items = createStore([
|
|
859
|
-
{ name: 'Charlie', score: 85 },
|
|
860
|
-
{ name: 'Alice', score: 95 },
|
|
861
|
-
{ name: 'Bob', score: 75 },
|
|
862
|
-
])
|
|
863
|
-
|
|
864
|
-
items.sort((a, b) => a.score - b.score)
|
|
865
|
-
|
|
866
|
-
// Verify order: Bob(75), Charlie(85), Alice(95)
|
|
867
|
-
expect(items[0].name.get()).toBe('Bob')
|
|
868
|
-
expect(items[1].name.get()).toBe('Charlie')
|
|
869
|
-
expect(items[2].name.get()).toBe('Alice')
|
|
870
|
-
|
|
871
|
-
// Verify signals are still reactive
|
|
872
|
-
items[0].score.set(100)
|
|
873
|
-
expect(items[0].score.get()).toBe(100)
|
|
874
|
-
})
|
|
875
|
-
|
|
876
|
-
test('default sort handles numbers as strings like Array.prototype.sort()', () => {
|
|
877
|
-
const numbers = createStore([10, 2, 1])
|
|
878
|
-
numbers.sort()
|
|
879
|
-
expect(numbers.get()).toEqual([1, 10, 2]) // String comparison: "1" < "10" < "2"
|
|
880
|
-
})
|
|
881
|
-
|
|
882
|
-
test('multiple sorts work correctly', () => {
|
|
883
|
-
const numbers = createStore([3, 1, 2])
|
|
884
|
-
numbers.sort((a, b) => a - b) // [1, 2, 3]
|
|
885
|
-
numbers.sort((a, b) => b - a) // [3, 2, 1]
|
|
886
|
-
expect(numbers.get()).toEqual([3, 2, 1])
|
|
887
|
-
})
|
|
888
482
|
})
|
|
889
483
|
|
|
890
484
|
describe('proxy behavior and enumeration', () => {
|
|
891
|
-
test('Object.keys returns property keys
|
|
892
|
-
// Record store
|
|
485
|
+
test('Object.keys returns property keys', () => {
|
|
893
486
|
const user = createStore({
|
|
894
|
-
name: '
|
|
895
|
-
email: '
|
|
487
|
+
name: 'Alice',
|
|
488
|
+
email: 'alice@example.com',
|
|
896
489
|
})
|
|
897
490
|
const userKeys = Object.keys(user)
|
|
898
491
|
expect(userKeys.sort()).toEqual(['email', 'name'])
|
|
899
|
-
|
|
900
|
-
// Array store
|
|
901
|
-
const numbers = createStore([1, 2, 3])
|
|
902
|
-
const numberKeys = Object.keys(numbers).filter(
|
|
903
|
-
k => !Number.isNaN(Number(k)),
|
|
904
|
-
)
|
|
905
|
-
expect(numberKeys).toEqual(['0', '1', '2'])
|
|
906
492
|
})
|
|
907
493
|
|
|
908
|
-
test('property enumeration works
|
|
909
|
-
// Record store
|
|
494
|
+
test('property enumeration works', () => {
|
|
910
495
|
const user = createStore({
|
|
911
|
-
name: '
|
|
912
|
-
email: '
|
|
496
|
+
name: 'Alice',
|
|
497
|
+
email: 'alice@example.com',
|
|
913
498
|
})
|
|
914
499
|
const userKeys: string[] = []
|
|
915
500
|
for (const key in user) {
|
|
916
501
|
userKeys.push(key)
|
|
917
502
|
}
|
|
918
503
|
expect(userKeys.sort()).toEqual(['email', 'name'])
|
|
919
|
-
|
|
920
|
-
// Array store
|
|
921
|
-
const numbers = createStore([10, 20])
|
|
922
|
-
const numberKeys: string[] = []
|
|
923
|
-
for (const key in numbers) {
|
|
924
|
-
if (!Number.isNaN(Number(key))) numberKeys.push(key)
|
|
925
|
-
}
|
|
926
|
-
expect(numberKeys).toEqual(['0', '1'])
|
|
927
504
|
})
|
|
928
505
|
|
|
929
|
-
test('in operator works
|
|
930
|
-
|
|
931
|
-
const user = createStore({ name: 'John' })
|
|
506
|
+
test('in operator works', () => {
|
|
507
|
+
const user = createStore({ name: 'Alice' })
|
|
932
508
|
expect('name' in user).toBe(true)
|
|
933
509
|
expect('email' in user).toBe(false)
|
|
934
|
-
expect('length' in user).toBe(true)
|
|
935
|
-
|
|
936
|
-
// Array store
|
|
937
|
-
const numbers = createStore([1, 2])
|
|
938
|
-
expect(0 in numbers).toBe(true)
|
|
939
|
-
expect(2 in numbers).toBe(false)
|
|
940
|
-
expect('length' in numbers).toBe(true)
|
|
941
510
|
})
|
|
942
511
|
|
|
943
|
-
test('Object.getOwnPropertyDescriptor works
|
|
944
|
-
|
|
945
|
-
const user = createStore({ name: 'John' })
|
|
512
|
+
test('Object.getOwnPropertyDescriptor works', () => {
|
|
513
|
+
const user = createStore({ name: 'Alice' })
|
|
946
514
|
const nameDescriptor = Object.getOwnPropertyDescriptor(user, 'name')
|
|
947
515
|
expect(nameDescriptor).toEqual({
|
|
948
516
|
enumerable: true,
|
|
@@ -950,195 +518,135 @@ describe('store', () => {
|
|
|
950
518
|
writable: true,
|
|
951
519
|
value: user.name,
|
|
952
520
|
})
|
|
521
|
+
})
|
|
522
|
+
})
|
|
953
523
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
// Array store
|
|
962
|
-
const numbers = createStore([1, 2])
|
|
963
|
-
const indexDescriptor = Object.getOwnPropertyDescriptor(
|
|
964
|
-
numbers,
|
|
965
|
-
'0',
|
|
966
|
-
)
|
|
967
|
-
expect(indexDescriptor).toEqual({
|
|
968
|
-
enumerable: true,
|
|
969
|
-
configurable: true,
|
|
970
|
-
writable: true,
|
|
971
|
-
value: numbers[0],
|
|
524
|
+
describe('byKey() method', () => {
|
|
525
|
+
test('works with property keys', () => {
|
|
526
|
+
const user = createStore({
|
|
527
|
+
name: 'Alice',
|
|
528
|
+
email: 'alice@example.com',
|
|
529
|
+
age: 30,
|
|
972
530
|
})
|
|
973
531
|
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
532
|
+
const nameSignal = user.byKey('name')
|
|
533
|
+
const emailSignal = user.byKey('email')
|
|
534
|
+
const ageSignal = user.byKey('age')
|
|
535
|
+
// @ts-expect-error deliberate access for nonexistent key
|
|
536
|
+
const nonexistentSignal = user.byKey('nonexistent')
|
|
537
|
+
|
|
538
|
+
expect(nameSignal?.get()).toBe('Alice')
|
|
539
|
+
expect(emailSignal?.get()).toBe('alice@example.com')
|
|
540
|
+
expect(ageSignal?.get()).toBe(30)
|
|
541
|
+
expect(nonexistentSignal).toBeUndefined()
|
|
542
|
+
|
|
543
|
+
// Verify these are the same signals as property access
|
|
544
|
+
expect(nameSignal).toBe(user.name)
|
|
545
|
+
expect(emailSignal).toBe(user.email)
|
|
546
|
+
expect(ageSignal).toBe(user.age)
|
|
980
547
|
})
|
|
981
|
-
})
|
|
982
548
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
expect(
|
|
992
|
-
|
|
993
|
-
// Array store - spreads signals (not [key, value] pairs)
|
|
994
|
-
const numbers = createStore([1, 2, 3])
|
|
995
|
-
const numberSpread = [...numbers]
|
|
996
|
-
expect(numberSpread).toHaveLength(3)
|
|
997
|
-
expect(typeof numberSpread[0].get).toBe('function')
|
|
998
|
-
expect(numberSpread[0].get()).toBe(1)
|
|
549
|
+
test('works with nested stores', () => {
|
|
550
|
+
const app = createStore({
|
|
551
|
+
config: {
|
|
552
|
+
version: '1.0.0',
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const configStore = app.byKey('config')
|
|
557
|
+
expect(configStore?.get()).toEqual({ version: '1.0.0' })
|
|
558
|
+
expect(configStore).toBe(app.config)
|
|
999
559
|
})
|
|
1000
560
|
|
|
1001
|
-
test('
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
561
|
+
test('is reactive and works with computed signals', () => {
|
|
562
|
+
const user = createStore<{
|
|
563
|
+
name: string
|
|
564
|
+
age: number
|
|
565
|
+
}>({
|
|
566
|
+
name: 'Alice',
|
|
567
|
+
age: 30,
|
|
568
|
+
})
|
|
1005
569
|
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
570
|
+
const nameSignal = user.byKey('name')
|
|
571
|
+
const displayName = new Memo(() =>
|
|
572
|
+
nameSignal ? `Hello, ${nameSignal.get()}!` : 'Unknown',
|
|
1009
573
|
)
|
|
1010
574
|
|
|
1011
|
-
expect(
|
|
1012
|
-
|
|
1013
|
-
expect(
|
|
1014
|
-
expect(combined[2].get()).toBe(3) // from store
|
|
1015
|
-
expect(combined[3].get()).toBe(4)
|
|
575
|
+
expect(displayName.get()).toBe('Hello, Alice!')
|
|
576
|
+
nameSignal?.set('Bob')
|
|
577
|
+
expect(displayName.get()).toBe('Hello, Bob!')
|
|
1016
578
|
})
|
|
1017
579
|
})
|
|
1018
580
|
|
|
1019
581
|
describe('UNSET and edge cases', () => {
|
|
1020
|
-
test('handles UNSET values
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
expect(recordData.value.get()).toBe(UNSET)
|
|
1024
|
-
recordData.value.set('some string')
|
|
1025
|
-
expect(recordData.value.get()).toBe('some string')
|
|
1026
|
-
|
|
1027
|
-
// Array store
|
|
1028
|
-
const arrayData = createStore([UNSET as string])
|
|
1029
|
-
expect(arrayData[0].get()).toBe(UNSET)
|
|
1030
|
-
arrayData[0].set('some value')
|
|
1031
|
-
expect(arrayData[0].get()).toBe('some value')
|
|
582
|
+
test('handles UNSET values', () => {
|
|
583
|
+
const store = createStore({ value: UNSET })
|
|
584
|
+
expect(store.get()).toEqual({ value: UNSET })
|
|
1032
585
|
})
|
|
1033
586
|
|
|
1034
|
-
test('handles primitive values
|
|
1035
|
-
|
|
1036
|
-
const recordData = createStore({
|
|
587
|
+
test('handles primitive values', () => {
|
|
588
|
+
const store = createStore({
|
|
1037
589
|
str: 'hello',
|
|
1038
590
|
num: 42,
|
|
1039
591
|
bool: true,
|
|
1040
592
|
})
|
|
1041
|
-
expect(
|
|
1042
|
-
expect(
|
|
1043
|
-
expect(
|
|
1044
|
-
|
|
1045
|
-
// Array store
|
|
1046
|
-
const arrayData = createStore(['hello', 42, true])
|
|
1047
|
-
expect(arrayData[0].get()).toBe('hello')
|
|
1048
|
-
expect(arrayData[1].get()).toBe(42)
|
|
1049
|
-
expect(arrayData[2].get()).toBe(true)
|
|
593
|
+
expect(store.str.get()).toBe('hello')
|
|
594
|
+
expect(store.num.get()).toBe(42)
|
|
595
|
+
expect(store.bool.get()).toBe(true)
|
|
1050
596
|
})
|
|
1051
597
|
|
|
1052
598
|
test('handles empty stores correctly', () => {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
expect(emptyRecord.length).toBe(0)
|
|
1056
|
-
expect(emptyRecord[Symbol.isConcatSpreadable]).toBe(false)
|
|
1057
|
-
expect([...emptyRecord]).toEqual([])
|
|
1058
|
-
|
|
1059
|
-
// Empty array store
|
|
1060
|
-
const emptyArray = createStore([])
|
|
1061
|
-
expect(emptyArray.length).toBe(0)
|
|
1062
|
-
expect(emptyArray[Symbol.isConcatSpreadable]).toBe(true)
|
|
1063
|
-
expect([...emptyArray]).toEqual([])
|
|
599
|
+
const empty = createStore({})
|
|
600
|
+
expect(empty.get()).toEqual({})
|
|
1064
601
|
})
|
|
1065
602
|
})
|
|
1066
603
|
|
|
1067
604
|
describe('JSON integration and serialization', () => {
|
|
1068
|
-
test('seamless JSON integration
|
|
1069
|
-
// Record store from JSON
|
|
605
|
+
test('seamless JSON integration', () => {
|
|
1070
606
|
const jsonData = {
|
|
1071
|
-
user: { name: '
|
|
607
|
+
user: { name: 'Alice', preferences: { theme: 'dark' } },
|
|
1072
608
|
settings: { timeout: 5000 },
|
|
1073
609
|
}
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
expect(
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
const serialized = JSON.stringify(recordStore.get())
|
|
610
|
+
const store = createStore(jsonData)
|
|
611
|
+
|
|
612
|
+
expect(store.user.name.get()).toBe('Alice')
|
|
613
|
+
expect(store.user.preferences.theme.get()).toBe('dark')
|
|
614
|
+
expect(store.settings.timeout.get()).toBe(5000)
|
|
615
|
+
|
|
616
|
+
const serialized = JSON.stringify(store.get())
|
|
1082
617
|
const parsed = JSON.parse(serialized)
|
|
1083
|
-
expect(parsed
|
|
1084
|
-
expect(parsed.settings.timeout).toBe(10000)
|
|
1085
|
-
|
|
1086
|
-
// Array store from JSON
|
|
1087
|
-
const arrayData = [
|
|
1088
|
-
{ id: 1, name: 'Item 1' },
|
|
1089
|
-
{ id: 2, name: 'Item 2' },
|
|
1090
|
-
]
|
|
1091
|
-
const arrayStore = createStore(arrayData)
|
|
1092
|
-
expect(arrayStore[0].name.get()).toBe('Item 1')
|
|
1093
|
-
|
|
1094
|
-
// Modify and serialize
|
|
1095
|
-
arrayStore[0].name.set('Updated Item')
|
|
1096
|
-
const arraySerialized = JSON.stringify(arrayStore.get())
|
|
1097
|
-
const arrayParsed = JSON.parse(arraySerialized)
|
|
1098
|
-
expect(arrayParsed[0].name).toBe('Updated Item')
|
|
618
|
+
expect(parsed).toEqual(jsonData)
|
|
1099
619
|
})
|
|
1100
620
|
|
|
1101
621
|
test('handles complex nested structures from JSON', () => {
|
|
1102
622
|
type Dashboard = {
|
|
1103
623
|
dashboard: {
|
|
1104
624
|
widgets: Array<{
|
|
1105
|
-
id:
|
|
625
|
+
id: string
|
|
1106
626
|
type: string
|
|
1107
|
-
config: {
|
|
1108
|
-
color?: string
|
|
1109
|
-
rows?: number
|
|
1110
|
-
}
|
|
627
|
+
config: { color?: string; rows?: number }
|
|
1111
628
|
}>
|
|
1112
629
|
}
|
|
1113
630
|
}
|
|
1114
|
-
|
|
631
|
+
|
|
632
|
+
const complexData: Dashboard = {
|
|
1115
633
|
dashboard: {
|
|
1116
634
|
widgets: [
|
|
1117
|
-
{ id: 1, type: 'chart', config: { color: 'blue' } },
|
|
1118
|
-
{ id: 2, type: 'table', config: { rows: 10 } },
|
|
635
|
+
{ id: '1', type: 'chart', config: { color: 'blue' } },
|
|
636
|
+
{ id: '2', type: 'table', config: { rows: 10 } },
|
|
1119
637
|
],
|
|
1120
638
|
},
|
|
1121
639
|
}
|
|
1122
640
|
|
|
1123
|
-
const store = createStore
|
|
1124
|
-
expect(store.dashboard.widgets
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
store.dashboard.widgets[0].config.color?.set('red')
|
|
1129
|
-
expect(store.get().dashboard.widgets[0].config.color).toBe('red')
|
|
641
|
+
const store = createStore(complexData)
|
|
642
|
+
expect(store.dashboard.widgets.at(0)?.get().config.color).toBe(
|
|
643
|
+
'blue',
|
|
644
|
+
)
|
|
645
|
+
expect(store.dashboard.widgets.at(1)?.get().config.rows).toBe(10)
|
|
1130
646
|
})
|
|
1131
647
|
})
|
|
1132
648
|
|
|
1133
649
|
describe('type conversion and nested stores', () => {
|
|
1134
|
-
test('arrays are converted to stores when nested', () => {
|
|
1135
|
-
const data = createStore({ items: [1, 2, 3] })
|
|
1136
|
-
expect(isStore(data.items)).toBe(true)
|
|
1137
|
-
expect(data.items[0].get()).toBe(1)
|
|
1138
|
-
expect(data.items[1].get()).toBe(2)
|
|
1139
|
-
expect(data.items[2].get()).toBe(3)
|
|
1140
|
-
})
|
|
1141
|
-
|
|
1142
650
|
test('nested objects become nested stores', () => {
|
|
1143
651
|
const config = createStore({
|
|
1144
652
|
database: {
|
|
@@ -1146,194 +654,10 @@ describe('store', () => {
|
|
|
1146
654
|
port: 5432,
|
|
1147
655
|
},
|
|
1148
656
|
})
|
|
657
|
+
|
|
1149
658
|
expect(isStore(config.database)).toBe(true)
|
|
1150
659
|
expect(config.database.host.get()).toBe('localhost')
|
|
1151
660
|
expect(config.database.port.get()).toBe(5432)
|
|
1152
661
|
})
|
|
1153
|
-
|
|
1154
|
-
test('array store with nested objects has correct type inference', () => {
|
|
1155
|
-
const users = createStore([
|
|
1156
|
-
{ name: 'Alice', active: true },
|
|
1157
|
-
{ name: 'Bob', active: false },
|
|
1158
|
-
])
|
|
1159
|
-
|
|
1160
|
-
// Object array elements should be Store<T>
|
|
1161
|
-
expect(isStore(users[0])).toBe(true)
|
|
1162
|
-
expect(isStore(users[1])).toBe(true)
|
|
1163
|
-
|
|
1164
|
-
// Should be able to access nested properties
|
|
1165
|
-
expect(users[0].name.get()).toBe('Alice')
|
|
1166
|
-
expect(users[0].active.get()).toBe(true)
|
|
1167
|
-
expect(users[1].name.get()).toBe('Bob')
|
|
1168
|
-
expect(users[1].active.get()).toBe(false)
|
|
1169
|
-
|
|
1170
|
-
// Should be able to modify nested properties
|
|
1171
|
-
users[0].name.set('Alicia')
|
|
1172
|
-
users[0].active.set(false)
|
|
1173
|
-
expect(users[0].name.get()).toBe('Alicia')
|
|
1174
|
-
expect(users[0].active.get()).toBe(false)
|
|
1175
|
-
})
|
|
1176
|
-
})
|
|
1177
|
-
|
|
1178
|
-
describe('advanced array behaviors', () => {
|
|
1179
|
-
test('array compaction with remove operations', () => {
|
|
1180
|
-
const numbers = createStore([10, 20, 30, 40, 50])
|
|
1181
|
-
|
|
1182
|
-
// Create computed to test both iteration and get() approaches
|
|
1183
|
-
const sumWithGet = createComputed(() => {
|
|
1184
|
-
const array = numbers.get()
|
|
1185
|
-
return array.reduce((acc, num) => acc + num, 0)
|
|
1186
|
-
})
|
|
1187
|
-
|
|
1188
|
-
expect(sumWithGet.get()).toBe(150) // 10+20+30+40+50
|
|
1189
|
-
|
|
1190
|
-
// Remove middle element - should compact the array
|
|
1191
|
-
numbers.remove(2) // Remove 30
|
|
1192
|
-
expect(numbers.length).toBe(4)
|
|
1193
|
-
expect(numbers.get()).toEqual([10, 20, 40, 50])
|
|
1194
|
-
expect(sumWithGet.get()).toBe(120) // 10+20+40+50
|
|
1195
|
-
|
|
1196
|
-
// Remove first element
|
|
1197
|
-
numbers.remove(0) // Remove 10
|
|
1198
|
-
expect(numbers.length).toBe(3)
|
|
1199
|
-
expect(numbers.get()).toEqual([20, 40, 50])
|
|
1200
|
-
expect(sumWithGet.get()).toBe(110) // 20+40+50
|
|
1201
|
-
})
|
|
1202
|
-
|
|
1203
|
-
test('sparse array replacement works correctly', () => {
|
|
1204
|
-
const numbers = createStore([10, 20, 30])
|
|
1205
|
-
|
|
1206
|
-
// Remove middle element to create sparse structure internally
|
|
1207
|
-
numbers.remove(1) // Remove 20, now [10, 30] with internal keys ["0", "2"]
|
|
1208
|
-
|
|
1209
|
-
expect(numbers.get()).toEqual([10, 30])
|
|
1210
|
-
expect(numbers.length).toBe(2)
|
|
1211
|
-
|
|
1212
|
-
// Set new array of same length - should work correctly
|
|
1213
|
-
numbers.set([100, 200])
|
|
1214
|
-
expect(numbers.get()).toEqual([100, 200])
|
|
1215
|
-
expect(numbers.length).toBe(2)
|
|
1216
|
-
expect(numbers[0].get()).toBe(100)
|
|
1217
|
-
expect(numbers[1].get()).toBe(200)
|
|
1218
|
-
})
|
|
1219
|
-
})
|
|
1220
|
-
|
|
1221
|
-
describe('polymorphic behavior determined at creation', () => {
|
|
1222
|
-
test('store type is determined at creation time and maintained', () => {
|
|
1223
|
-
// Array store stays array-like
|
|
1224
|
-
const arrayStore = createStore([1, 2])
|
|
1225
|
-
expect(arrayStore[Symbol.isConcatSpreadable]).toBe(true)
|
|
1226
|
-
expect(arrayStore.length).toBe(2)
|
|
1227
|
-
|
|
1228
|
-
// Even after modifications, stays array-like
|
|
1229
|
-
arrayStore.add(3)
|
|
1230
|
-
expect(arrayStore[Symbol.isConcatSpreadable]).toBe(true)
|
|
1231
|
-
expect(arrayStore.length).toBe(3)
|
|
1232
|
-
|
|
1233
|
-
// Record store stays record-like
|
|
1234
|
-
const recordStore = createStore<{
|
|
1235
|
-
a: number
|
|
1236
|
-
b: number
|
|
1237
|
-
c?: number
|
|
1238
|
-
}>({ a: 1, b: 2 })
|
|
1239
|
-
expect(recordStore[Symbol.isConcatSpreadable]).toBe(false)
|
|
1240
|
-
expect(recordStore.length).toBe(2)
|
|
1241
|
-
|
|
1242
|
-
// Even after modifications, stays record-like
|
|
1243
|
-
recordStore.add('c', 3)
|
|
1244
|
-
expect(recordStore[Symbol.isConcatSpreadable]).toBe(false)
|
|
1245
|
-
expect(recordStore.length).toBe(3)
|
|
1246
|
-
})
|
|
1247
|
-
|
|
1248
|
-
test('empty stores maintain their type characteristics', () => {
|
|
1249
|
-
const emptyArray = createStore<string[]>([])
|
|
1250
|
-
const emptyRecord = createStore<{ key?: string }>({})
|
|
1251
|
-
|
|
1252
|
-
// Empty array behaves like array
|
|
1253
|
-
expect(emptyArray[Symbol.isConcatSpreadable]).toBe(true)
|
|
1254
|
-
expect(emptyArray.length).toBe(0)
|
|
1255
|
-
|
|
1256
|
-
// Empty record behaves like record
|
|
1257
|
-
expect(emptyRecord[Symbol.isConcatSpreadable]).toBe(false)
|
|
1258
|
-
expect(emptyRecord.length).toBe(0)
|
|
1259
|
-
|
|
1260
|
-
// After adding items, they maintain their characteristics
|
|
1261
|
-
emptyArray.add('first')
|
|
1262
|
-
emptyRecord.add('key', 'value')
|
|
1263
|
-
|
|
1264
|
-
expect(emptyArray[Symbol.isConcatSpreadable]).toBe(true)
|
|
1265
|
-
expect(emptyRecord[Symbol.isConcatSpreadable]).toBe(false)
|
|
1266
|
-
})
|
|
1267
|
-
})
|
|
1268
|
-
|
|
1269
|
-
describe('cross-component communication pattern', () => {
|
|
1270
|
-
test('event bus with UNSET initialization - type-safe pattern', () => {
|
|
1271
|
-
type EventBusSchema = {
|
|
1272
|
-
userLogin: { userId: number; timestamp: number }
|
|
1273
|
-
userLogout: { userId: number }
|
|
1274
|
-
userUpdate: { userId: number; profile: { name: string } }
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
const eventBus = createStore<EventBusSchema>({
|
|
1278
|
-
userLogin: UNSET,
|
|
1279
|
-
userLogout: UNSET,
|
|
1280
|
-
userUpdate: UNSET,
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
const on = (
|
|
1284
|
-
event: keyof EventBusSchema,
|
|
1285
|
-
callback: (data: EventBusSchema[keyof EventBusSchema]) => void,
|
|
1286
|
-
) =>
|
|
1287
|
-
createEffect(() => {
|
|
1288
|
-
const data = eventBus[event].get()
|
|
1289
|
-
if (data !== UNSET) callback(data)
|
|
1290
|
-
})
|
|
1291
|
-
|
|
1292
|
-
let receivedLogin: unknown = null
|
|
1293
|
-
let receivedLogout: unknown = null
|
|
1294
|
-
let receivedUpdate: unknown = null
|
|
1295
|
-
|
|
1296
|
-
on('userLogin', data => {
|
|
1297
|
-
receivedLogin = data
|
|
1298
|
-
})
|
|
1299
|
-
on('userLogout', data => {
|
|
1300
|
-
receivedLogout = data
|
|
1301
|
-
})
|
|
1302
|
-
on('userUpdate', data => {
|
|
1303
|
-
receivedUpdate = data
|
|
1304
|
-
})
|
|
1305
|
-
|
|
1306
|
-
// Initially nothing received
|
|
1307
|
-
expect(receivedLogin).toBe(null)
|
|
1308
|
-
expect(receivedLogout).toBe(null)
|
|
1309
|
-
expect(receivedUpdate).toBe(null)
|
|
1310
|
-
|
|
1311
|
-
// Emit events
|
|
1312
|
-
const loginData: EventBusSchema['userLogin'] = {
|
|
1313
|
-
userId: 123,
|
|
1314
|
-
timestamp: Date.now(),
|
|
1315
|
-
}
|
|
1316
|
-
eventBus.userLogin.set(loginData)
|
|
1317
|
-
|
|
1318
|
-
expect(receivedLogin).toEqual(loginData)
|
|
1319
|
-
expect(receivedLogout).toBe(null)
|
|
1320
|
-
expect(receivedUpdate).toBe(null)
|
|
1321
|
-
|
|
1322
|
-
const logoutData: EventBusSchema['userLogout'] = { userId: 123 }
|
|
1323
|
-
eventBus.userLogout.set(logoutData)
|
|
1324
|
-
|
|
1325
|
-
expect(receivedLogout).toEqual(logoutData)
|
|
1326
|
-
expect(receivedLogin).toEqual(loginData) // unchanged
|
|
1327
|
-
|
|
1328
|
-
const updateData: EventBusSchema['userUpdate'] = {
|
|
1329
|
-
userId: 456,
|
|
1330
|
-
profile: { name: 'Alice' },
|
|
1331
|
-
}
|
|
1332
|
-
eventBus.userUpdate.set(updateData)
|
|
1333
|
-
|
|
1334
|
-
expect(receivedUpdate).toEqual(updateData)
|
|
1335
|
-
expect(receivedLogin).toEqual(loginData) // unchanged
|
|
1336
|
-
expect(receivedLogout).toEqual(logoutData) // unchanged
|
|
1337
|
-
})
|
|
1338
662
|
})
|
|
1339
663
|
})
|