@zeix/cause-effect 0.17.3 → 0.18.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 +163 -232
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +199 -143
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +232 -197
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1325 -997
- package/index.js +1 -1
- package/index.ts +58 -74
- package/package.json +4 -1
- package/src/errors.ts +118 -74
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +466 -706
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createSensor,
|
|
6
|
+
isMemo,
|
|
7
|
+
isSensor,
|
|
8
|
+
SKIP_EQUALITY,
|
|
9
|
+
UnsetSignalValueError,
|
|
10
|
+
} from '../index.ts'
|
|
11
|
+
|
|
12
|
+
/* === Utility Functions === */
|
|
13
|
+
|
|
14
|
+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
15
|
+
|
|
16
|
+
/* === Tests === */
|
|
17
|
+
|
|
18
|
+
describe('Sensor', () => {
|
|
19
|
+
describe('createSensor', () => {
|
|
20
|
+
test('should have Symbol.toStringTag of "Sensor"', () => {
|
|
21
|
+
const sensor = createSensor<number>(() => () => {})
|
|
22
|
+
expect(sensor[Symbol.toStringTag]).toBe('Sensor')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('should throw UnsetSignalValueError when read outside an effect', () => {
|
|
26
|
+
const sensor = createSensor<number>(set => {
|
|
27
|
+
set(42)
|
|
28
|
+
return () => {}
|
|
29
|
+
})
|
|
30
|
+
expect(() => sensor.get()).toThrow(UnsetSignalValueError)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('should activate and return value when read inside an effect', () => {
|
|
34
|
+
let started = false
|
|
35
|
+
const sensor = createSensor<number>(set => {
|
|
36
|
+
started = true
|
|
37
|
+
set(42)
|
|
38
|
+
return () => {}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(started).toBe(false)
|
|
42
|
+
|
|
43
|
+
let received: number | undefined
|
|
44
|
+
createEffect(() => {
|
|
45
|
+
received = sensor.get()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(started).toBe(true)
|
|
49
|
+
expect(received).toBe(42)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('isSensor', () => {
|
|
54
|
+
test('should identify sensor signals', () => {
|
|
55
|
+
expect(isSensor(createSensor<number>(() => () => {}))).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('should return false for non-sensor values', () => {
|
|
59
|
+
expect(isSensor(42)).toBe(false)
|
|
60
|
+
expect(isSensor(null)).toBe(false)
|
|
61
|
+
expect(isSensor({})).toBe(false)
|
|
62
|
+
expect(isMemo(createSensor<number>(() => () => {}))).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('start/link ordering', () => {
|
|
67
|
+
test('synchronous set() inside start should be visible to activating effect', () => {
|
|
68
|
+
// Start fires before link: synchronous set() updates node.value
|
|
69
|
+
// without propagation (no sinks yet). The activating effect reads
|
|
70
|
+
// the updated value directly after link completes.
|
|
71
|
+
const sensor = createSensor<number>(
|
|
72
|
+
set => {
|
|
73
|
+
set(10)
|
|
74
|
+
return () => {}
|
|
75
|
+
},
|
|
76
|
+
{ value: 0 },
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const doubled = createMemo(() => sensor.get() * 2)
|
|
80
|
+
|
|
81
|
+
let result = 0
|
|
82
|
+
createEffect(() => {
|
|
83
|
+
result = doubled.get()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// The memo should see 10 (from start's set), not 0 (initial value)
|
|
87
|
+
expect(result).toBe(20)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('set callback', () => {
|
|
92
|
+
test('should update value and trigger effects', () => {
|
|
93
|
+
let setFn!: (v: number) => void
|
|
94
|
+
const sensor = createSensor<number>(set => {
|
|
95
|
+
setFn = set
|
|
96
|
+
set(0)
|
|
97
|
+
return () => {}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
let effectCount = 0
|
|
101
|
+
let received = 0
|
|
102
|
+
createEffect(() => {
|
|
103
|
+
received = sensor.get()
|
|
104
|
+
effectCount++
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
expect(received).toBe(0)
|
|
108
|
+
expect(effectCount).toBe(1)
|
|
109
|
+
|
|
110
|
+
setFn(10)
|
|
111
|
+
expect(received).toBe(10)
|
|
112
|
+
expect(effectCount).toBe(2)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('should notify multiple effects', () => {
|
|
116
|
+
let setFn!: (v: string) => void
|
|
117
|
+
const sensor = createSensor<string>(set => {
|
|
118
|
+
setFn = set
|
|
119
|
+
set('initial')
|
|
120
|
+
return () => {}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const mock1 = mock(() => {})
|
|
124
|
+
const mock2 = mock(() => {})
|
|
125
|
+
|
|
126
|
+
createEffect(() => {
|
|
127
|
+
sensor.get()
|
|
128
|
+
mock1()
|
|
129
|
+
})
|
|
130
|
+
createEffect(() => {
|
|
131
|
+
sensor.get()
|
|
132
|
+
mock2()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(mock1).toHaveBeenCalledTimes(1)
|
|
136
|
+
expect(mock2).toHaveBeenCalledTimes(1)
|
|
137
|
+
|
|
138
|
+
setFn('updated')
|
|
139
|
+
expect(mock1).toHaveBeenCalledTimes(2)
|
|
140
|
+
expect(mock2).toHaveBeenCalledTimes(2)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('Lazy Activation', () => {
|
|
145
|
+
test('should only call start when first effect subscribes', async () => {
|
|
146
|
+
let counter = 0
|
|
147
|
+
let intervalId: Timer | undefined
|
|
148
|
+
|
|
149
|
+
const sensor = createSensor<number>(set => {
|
|
150
|
+
set(0)
|
|
151
|
+
intervalId = setInterval(() => {
|
|
152
|
+
counter++
|
|
153
|
+
set(counter)
|
|
154
|
+
}, 10)
|
|
155
|
+
return () => {
|
|
156
|
+
clearInterval(intervalId)
|
|
157
|
+
intervalId = undefined
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(counter).toBe(0)
|
|
162
|
+
await wait(50)
|
|
163
|
+
expect(counter).toBe(0)
|
|
164
|
+
expect(intervalId).toBeUndefined()
|
|
165
|
+
|
|
166
|
+
const dispose = createEffect(() => {
|
|
167
|
+
sensor.get()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await wait(50)
|
|
171
|
+
expect(counter).toBeGreaterThan(0)
|
|
172
|
+
expect(intervalId).toBeDefined()
|
|
173
|
+
|
|
174
|
+
dispose()
|
|
175
|
+
const counterAfterStop = counter
|
|
176
|
+
|
|
177
|
+
await wait(50)
|
|
178
|
+
expect(counter).toBe(counterAfterStop)
|
|
179
|
+
expect(intervalId).toBeUndefined()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('should call start only once for multiple subscribers', () => {
|
|
183
|
+
let startCount = 0
|
|
184
|
+
let cleanupCount = 0
|
|
185
|
+
|
|
186
|
+
const sensor = createSensor<number>(set => {
|
|
187
|
+
startCount++
|
|
188
|
+
set(1)
|
|
189
|
+
return () => {
|
|
190
|
+
cleanupCount++
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const dispose1 = createEffect(() => {
|
|
195
|
+
sensor.get()
|
|
196
|
+
})
|
|
197
|
+
expect(startCount).toBe(1)
|
|
198
|
+
|
|
199
|
+
const dispose2 = createEffect(() => {
|
|
200
|
+
sensor.get()
|
|
201
|
+
})
|
|
202
|
+
expect(startCount).toBe(1)
|
|
203
|
+
|
|
204
|
+
dispose1()
|
|
205
|
+
expect(cleanupCount).toBe(0) // still has subscriber
|
|
206
|
+
|
|
207
|
+
dispose2()
|
|
208
|
+
expect(cleanupCount).toBe(1)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('should reactivate after all subscribers leave and new one arrives', () => {
|
|
212
|
+
let startCount = 0
|
|
213
|
+
let cleanupCount = 0
|
|
214
|
+
|
|
215
|
+
const sensor = createSensor<number>(set => {
|
|
216
|
+
startCount++
|
|
217
|
+
set(startCount)
|
|
218
|
+
return () => {
|
|
219
|
+
cleanupCount++
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const dispose1 = createEffect(() => {
|
|
224
|
+
sensor.get()
|
|
225
|
+
})
|
|
226
|
+
expect(startCount).toBe(1)
|
|
227
|
+
|
|
228
|
+
dispose1()
|
|
229
|
+
expect(cleanupCount).toBe(1)
|
|
230
|
+
|
|
231
|
+
let received = 0
|
|
232
|
+
const dispose2 = createEffect(() => {
|
|
233
|
+
received = sensor.get()
|
|
234
|
+
})
|
|
235
|
+
expect(startCount).toBe(2)
|
|
236
|
+
expect(received).toBe(2)
|
|
237
|
+
|
|
238
|
+
dispose2()
|
|
239
|
+
expect(cleanupCount).toBe(2)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('options.equals', () => {
|
|
244
|
+
test('should skip update when value is equal by default', () => {
|
|
245
|
+
let setFn!: (v: number) => void
|
|
246
|
+
const sensor = createSensor<number>(set => {
|
|
247
|
+
setFn = set
|
|
248
|
+
set(5)
|
|
249
|
+
return () => {}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
let effectCount = 0
|
|
253
|
+
createEffect(() => {
|
|
254
|
+
sensor.get()
|
|
255
|
+
effectCount++
|
|
256
|
+
})
|
|
257
|
+
expect(effectCount).toBe(1)
|
|
258
|
+
|
|
259
|
+
setFn(5) // same value
|
|
260
|
+
expect(effectCount).toBe(1)
|
|
261
|
+
|
|
262
|
+
setFn(6)
|
|
263
|
+
expect(effectCount).toBe(2)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('should use custom equality function', () => {
|
|
267
|
+
let setFn!: (v: { x: number }) => void
|
|
268
|
+
const sensor = createSensor<{ x: number }>(
|
|
269
|
+
set => {
|
|
270
|
+
setFn = set
|
|
271
|
+
set({ x: 1 })
|
|
272
|
+
return () => {}
|
|
273
|
+
},
|
|
274
|
+
{ equals: (a, b) => a?.x === b?.x },
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
let effectCount = 0
|
|
278
|
+
createEffect(() => {
|
|
279
|
+
sensor.get()
|
|
280
|
+
effectCount++
|
|
281
|
+
})
|
|
282
|
+
expect(effectCount).toBe(1)
|
|
283
|
+
|
|
284
|
+
setFn({ x: 1 }) // structurally equal
|
|
285
|
+
expect(effectCount).toBe(1)
|
|
286
|
+
|
|
287
|
+
setFn({ x: 2 })
|
|
288
|
+
expect(effectCount).toBe(2)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
describe('options.guard', () => {
|
|
293
|
+
test('should validate values from set callback', () => {
|
|
294
|
+
let setFn!: (v: number) => void
|
|
295
|
+
const sensor = createSensor<number>(
|
|
296
|
+
set => {
|
|
297
|
+
setFn = set
|
|
298
|
+
set(1)
|
|
299
|
+
return () => {}
|
|
300
|
+
},
|
|
301
|
+
{ guard: (v): v is number => typeof v === 'number' && v > 0 },
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
createEffect(() => {
|
|
305
|
+
sensor.get()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
expect(() => setFn(5)).not.toThrow()
|
|
309
|
+
expect(() => setFn(-1)).toThrow()
|
|
310
|
+
expect(() => setFn(0)).toThrow()
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('options.value', () => {
|
|
315
|
+
test('should use initial value before activation', () => {
|
|
316
|
+
const sensor = createSensor<number>(() => () => {}, { value: 99 })
|
|
317
|
+
|
|
318
|
+
let received: number | undefined
|
|
319
|
+
createEffect(() => {
|
|
320
|
+
received = sensor.get()
|
|
321
|
+
})
|
|
322
|
+
expect(received).toBe(99)
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('SKIP_EQUALITY', () => {
|
|
327
|
+
test('should always return false', () => {
|
|
328
|
+
expect(SKIP_EQUALITY()).toBe(false)
|
|
329
|
+
expect(SKIP_EQUALITY(1, 1)).toBe(false)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('should return the reference value from get()', () => {
|
|
333
|
+
const obj = { name: 'test' }
|
|
334
|
+
const sensor = createSensor<typeof obj>(
|
|
335
|
+
set => {
|
|
336
|
+
set(obj)
|
|
337
|
+
return () => {}
|
|
338
|
+
},
|
|
339
|
+
{ value: obj, equals: SKIP_EQUALITY },
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
let received: typeof obj | undefined
|
|
343
|
+
const dispose = createEffect(() => {
|
|
344
|
+
received = sensor.get()
|
|
345
|
+
})
|
|
346
|
+
expect(received).toBe(obj)
|
|
347
|
+
dispose()
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('should re-run effects when set is called with same reference', () => {
|
|
351
|
+
const obj = { status: 'offline' }
|
|
352
|
+
let setFn!: (next: typeof obj) => void
|
|
353
|
+
|
|
354
|
+
const sensor = createSensor<typeof obj>(
|
|
355
|
+
set => {
|
|
356
|
+
setFn = set
|
|
357
|
+
set(obj)
|
|
358
|
+
return () => {}
|
|
359
|
+
},
|
|
360
|
+
{ equals: SKIP_EQUALITY },
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
let effectCount = 0
|
|
364
|
+
let lastStatus = ''
|
|
365
|
+
createEffect(() => {
|
|
366
|
+
lastStatus = sensor.get().status
|
|
367
|
+
effectCount++
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
expect(effectCount).toBe(1)
|
|
371
|
+
expect(lastStatus).toBe('offline')
|
|
372
|
+
|
|
373
|
+
obj.status = 'online'
|
|
374
|
+
expect(effectCount).toBe(1) // no set yet
|
|
375
|
+
|
|
376
|
+
setFn(obj) // same reference, but SKIP_EQUALITY ensures propagation
|
|
377
|
+
expect(effectCount).toBe(2)
|
|
378
|
+
expect(lastStatus).toBe('online')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('should trigger multiple effect runs on multiple set calls', () => {
|
|
382
|
+
const obj = { size: 100 }
|
|
383
|
+
let setFn!: (next: typeof obj) => void
|
|
384
|
+
|
|
385
|
+
const sensor = createSensor<typeof obj>(
|
|
386
|
+
set => {
|
|
387
|
+
setFn = set
|
|
388
|
+
set(obj)
|
|
389
|
+
return () => {}
|
|
390
|
+
},
|
|
391
|
+
{ equals: SKIP_EQUALITY },
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const callback = mock(() => {})
|
|
395
|
+
createEffect(() => {
|
|
396
|
+
sensor.get()
|
|
397
|
+
callback()
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
401
|
+
|
|
402
|
+
setFn(obj)
|
|
403
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
404
|
+
|
|
405
|
+
setFn(obj)
|
|
406
|
+
expect(callback).toHaveBeenCalledTimes(3)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('should validate values passed through set()', () => {
|
|
410
|
+
let setFn!: (next: unknown) => void
|
|
411
|
+
|
|
412
|
+
const sensor = createSensor<{ x: number }>(
|
|
413
|
+
set => {
|
|
414
|
+
setFn = set as (next: unknown) => void
|
|
415
|
+
set({ x: 1 })
|
|
416
|
+
return () => {}
|
|
417
|
+
},
|
|
418
|
+
{ equals: SKIP_EQUALITY },
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
createEffect(() => {
|
|
422
|
+
sensor.get()
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
expect(() => {
|
|
426
|
+
// biome-ignore lint/suspicious/noExplicitAny: testing invalid input
|
|
427
|
+
setFn(null as any)
|
|
428
|
+
}).toThrow('[Sensor] Signal value cannot be null or undefined')
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
describe('Input Validation', () => {
|
|
433
|
+
test('should throw InvalidCallbackError for non-function start', () => {
|
|
434
|
+
expect(() => {
|
|
435
|
+
// @ts-expect-error - Testing invalid input
|
|
436
|
+
createSensor(null)
|
|
437
|
+
}).toThrow('[Sensor] Callback null is invalid')
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test('should throw InvalidCallbackError for async start callback', () => {
|
|
441
|
+
expect(() => {
|
|
442
|
+
// @ts-expect-error - Testing invalid input
|
|
443
|
+
createSensor(async () => () => {})
|
|
444
|
+
}).toThrow()
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('should throw NullishSignalValueError for null initial value', () => {
|
|
448
|
+
expect(() => {
|
|
449
|
+
// @ts-expect-error - Testing invalid input
|
|
450
|
+
createSensor<number>(() => () => {}, { value: null })
|
|
451
|
+
}).toThrow('[Sensor] Signal value cannot be null or undefined')
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
})
|