@zeix/cause-effect 0.16.1 → 0.17.1
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 +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -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 +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -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 +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +853 -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/ref.test.ts +227 -0
- 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 +12 -9
- package/types/src/classes/collection.d.ts +46 -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/ref.d.ts +39 -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 +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- 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/CLAUDE.md
CHANGED
|
@@ -6,20 +6,20 @@ Cause & Effect is a reactive state management library that implements the signal
|
|
|
6
6
|
|
|
7
7
|
### Core Philosophy
|
|
8
8
|
- **Granular Reactivity**: Changes propagate only to dependent computations, minimizing unnecessary work
|
|
9
|
-
- **Explicit Dependencies**:
|
|
10
|
-
- **
|
|
11
|
-
- **Type Safety First**: Comprehensive TypeScript support with strict generic constraints
|
|
9
|
+
- **Explicit Dependencies**: Dependencies are tracked through `.get()` calls, no hidden subscriptions
|
|
10
|
+
- **Type Safety First**: Comprehensive TypeScript support with strict generic constraints (`T extends {}`)
|
|
12
11
|
- **Performance Conscious**: Minimal overhead through efficient dependency tracking and batching
|
|
13
12
|
|
|
14
13
|
## Mental Model for Understanding the System
|
|
15
14
|
|
|
16
15
|
Think of signals as **observable cells** in a spreadsheet:
|
|
17
|
-
- **State signals** are
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
- **State signals** are input cells for primitive values and objects
|
|
17
|
+
- **Ref signals** are cells that reference external objects (DOM, Map, Set) requiring manual notification
|
|
18
|
+
- **Computed signals** are formula cells that automatically recalculate when dependencies change
|
|
19
|
+
- **Store signals** are structured tables where individual columns (properties) are reactive
|
|
20
|
+
- **List signals** are tables with stable row IDs that survive sorting and reordering
|
|
21
|
+
- **Collection signals** are read-only derived tables with item-level memoization
|
|
22
|
+
- **Effects** are event handlers that trigger side effects when cells change
|
|
23
23
|
|
|
24
24
|
## Architectural Deep Dive
|
|
25
25
|
|
|
@@ -43,24 +43,53 @@ interface MutableSignal<T extends {}> extends Signal<T> {
|
|
|
43
43
|
set(value: T): void
|
|
44
44
|
update(fn: (current: T) => T): void
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
// Collection interface - implemented by various collection types
|
|
48
|
+
interface Collection<T extends {}> {
|
|
49
|
+
get(): T[]
|
|
50
|
+
at(index: number): Signal<T> | undefined
|
|
51
|
+
byKey(key: string): Signal<T> | undefined
|
|
52
|
+
deriveCollection<R extends {}>(callback: CollectionCallback<R, T>): Collection<R>
|
|
53
|
+
}
|
|
46
54
|
```
|
|
47
55
|
|
|
48
56
|
The generic constraint `T extends {}` is crucial - it excludes `null` and `undefined` at the type level, preventing common runtime errors and making the API more predictable.
|
|
49
57
|
|
|
50
|
-
###
|
|
58
|
+
### Collection Architecture
|
|
59
|
+
|
|
60
|
+
Collections are an interface implemented by different reactive array types:
|
|
61
|
+
|
|
62
|
+
**DerivedCollection**: Read-only transformations of Lists or other Collections
|
|
63
|
+
- Item-level memoization with Computed signals
|
|
64
|
+
- Async support with automatic cancellation
|
|
65
|
+
- Chainable for data pipelines
|
|
66
|
+
|
|
67
|
+
**ElementCollection**: DOM element collections with MutationObserver
|
|
68
|
+
- Uses Ref signals for elements that change externally
|
|
69
|
+
- Watches attributes and childList mutations
|
|
70
|
+
- Stable keys for persistent element identity
|
|
71
|
+
|
|
72
|
+
Key patterns:
|
|
73
|
+
- Collections return arrays of values via `.get()`
|
|
74
|
+
- Individual items accessed as signals via `.at()` and `.byKey()`
|
|
75
|
+
- All collections support `.deriveCollection()` for chaining
|
|
51
76
|
|
|
52
|
-
Store
|
|
77
|
+
### Store and List Architecture
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
**Store signals** (`createStore`): Transform objects into reactive data structures
|
|
80
|
+
- Each property becomes its own signal via Proxy
|
|
81
|
+
- Built on `Composite` class for signal management
|
|
82
|
+
- Dynamic property addition/removal with proper reactivity
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
|
|
84
|
+
**List signals** (`new List`): Arrays with stable keys and reactive items
|
|
85
|
+
- Maintains stable keys that survive sorting and reordering
|
|
86
|
+
- Built on `Composite` class for consistent signal management
|
|
87
|
+
- Provides `byKey()`, `keyAt()`, `indexOfKey()` for key-based access
|
|
88
|
+
|
|
89
|
+
**Composite Architecture**: Shared foundation for Store and List
|
|
90
|
+
- `Map<string, Signal>` for property/item signals
|
|
91
|
+
- Event system for granular add/change/remove notifications
|
|
92
|
+
- Lazy signal creation and automatic cleanup
|
|
64
93
|
|
|
65
94
|
### Computed Signal Memoization Strategy
|
|
66
95
|
|
|
@@ -69,61 +98,92 @@ Computed signals implement smart memoization:
|
|
|
69
98
|
- **Stale Detection**: Only recalculates when dependencies actually change
|
|
70
99
|
- **Async Support**: Handles Promise-based computations with automatic cancellation
|
|
71
100
|
- **Error Handling**: Preserves error states and prevents cascade failures
|
|
72
|
-
- **Reducer
|
|
101
|
+
- **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
|
|
73
102
|
|
|
74
103
|
## Advanced Patterns and Best Practices
|
|
75
104
|
|
|
76
105
|
### When to Use Each Signal Type
|
|
77
106
|
|
|
78
|
-
**State Signals (`
|
|
107
|
+
**State Signals (`State`)**:
|
|
79
108
|
- Primitive values (numbers, strings, booleans)
|
|
80
109
|
- Objects that you replace entirely rather than mutating properties
|
|
81
110
|
- Simple toggles and flags
|
|
82
111
|
- Values with straightforward update patterns
|
|
83
112
|
|
|
84
113
|
```typescript
|
|
85
|
-
const count =
|
|
86
|
-
const theme =
|
|
87
|
-
|
|
114
|
+
const count = new State(0)
|
|
115
|
+
const theme = new State<'light' | 'dark'>('light')
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Ref Signals (`new Ref`)**:
|
|
119
|
+
- External objects that change outside the reactive system
|
|
120
|
+
- DOM elements, Map, Set, Date objects, third-party APIs
|
|
121
|
+
- Requires manual `.notify()` when external object changes
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
125
|
+
const cacheRef = new Ref(new Map())
|
|
126
|
+
// When external change occurs: cacheRef.notify()
|
|
88
127
|
```
|
|
89
128
|
|
|
90
129
|
**Store Signals (`createStore`)**:
|
|
91
130
|
- Objects where individual properties change independently
|
|
92
|
-
- Nested data structures
|
|
93
|
-
- Form state where fields update individually
|
|
94
|
-
- Configuration objects with multiple settings
|
|
95
131
|
|
|
96
132
|
```typescript
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
const user = createStore({ name: 'Alice', email: 'alice@example.com' })
|
|
134
|
+
user.name.set('Bob') // Only name subscribers react
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**List Signals (`new List`)**:
|
|
138
|
+
```typescript
|
|
139
|
+
const todoList = new List([
|
|
140
|
+
{ id: 'task1', text: 'Learn signals' }
|
|
141
|
+
], todo => todo.id)
|
|
142
|
+
const firstTodo = todoList.byKey('task1') // Access by stable key
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Collection Signals (`new DerivedCollection`)**:
|
|
146
|
+
```typescript
|
|
147
|
+
const completedTodos = new DerivedCollection(todoList, todo =>
|
|
148
|
+
todo.completed ? { ...todo, status: 'done' } : null
|
|
149
|
+
)
|
|
150
|
+
const todoDetails = new DerivedCollection(todoList, async (todo, abort) => {
|
|
151
|
+
const response = await fetch(`/todos/${todo.id}`, { signal: abort })
|
|
152
|
+
return { ...todo, details: await response.json() }
|
|
101
153
|
})
|
|
102
|
-
|
|
154
|
+
|
|
155
|
+
// Chain collections for data pipelines
|
|
156
|
+
const urgentTodoSummaries = todoList
|
|
157
|
+
.deriveCollection(todo => ({ ...todo, urgent: todo.priority > 8 }))
|
|
158
|
+
.deriveCollection(todo => todo.urgent ? `URGENT: ${todo.text}` : todo.text)
|
|
159
|
+
|
|
160
|
+
// Collections maintain stable references through List changes
|
|
161
|
+
const firstTodoDetail = todoDetails.byKey('task1') // Computed signal
|
|
162
|
+
todoList.sort() // Reorders list but collection signals remain stable
|
|
103
163
|
```
|
|
104
164
|
|
|
105
|
-
**Computed Signals (`
|
|
165
|
+
**Computed Signals (`Memo` and `Task`)**:
|
|
106
166
|
- Expensive calculations that should be memoized
|
|
107
167
|
- Derived data that depends on multiple signals
|
|
108
168
|
- Async operations that need automatic cancellation
|
|
109
169
|
- Cross-cutting concerns that multiple components need
|
|
110
170
|
|
|
111
171
|
```typescript
|
|
112
|
-
const expensiveCalc =
|
|
172
|
+
const expensiveCalc = new Memo(() => {
|
|
113
173
|
return heavyComputation(data1.get(), data2.get()) // Memoized
|
|
114
174
|
})
|
|
115
175
|
|
|
116
|
-
const userData =
|
|
176
|
+
const userData = new Task(async (prev, abort) => {
|
|
117
177
|
const id = userId.get()
|
|
118
178
|
if (!id) return prev // Keep previous data if no ID
|
|
119
179
|
const response = await fetch(`/users/${id}`, { signal: abort })
|
|
120
180
|
return response.json()
|
|
121
181
|
})
|
|
122
182
|
|
|
123
|
-
// Reducer
|
|
124
|
-
const gameState =
|
|
183
|
+
// Reducer pattern for state machines
|
|
184
|
+
const gameState = new Memo(prev => {
|
|
125
185
|
const action = playerAction.get()
|
|
126
|
-
switch (
|
|
186
|
+
switch (prev) {
|
|
127
187
|
case 'menu':
|
|
128
188
|
return action === 'start' ? 'playing' : 'menu'
|
|
129
189
|
case 'playing':
|
|
@@ -138,7 +198,7 @@ const gameState = createComputed((currentState) => {
|
|
|
138
198
|
}, 'menu') // Initial state
|
|
139
199
|
|
|
140
200
|
// Accumulating values over time
|
|
141
|
-
const runningTotal =
|
|
201
|
+
const runningTotal = new Memo(prev => {
|
|
142
202
|
const newValue = currentValue.get()
|
|
143
203
|
return previous + newValue
|
|
144
204
|
}, 0) // Start with 0
|
|
@@ -154,19 +214,13 @@ The library provides several layers of error handling:
|
|
|
154
214
|
4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
|
|
155
215
|
|
|
156
216
|
```typescript
|
|
157
|
-
//
|
|
158
|
-
const apiData =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return response.json()
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (abort.aborted) return UNSET // Cancelled, not an error
|
|
165
|
-
throw error // Real error
|
|
166
|
-
}
|
|
217
|
+
// Error handling with resolve() and match()
|
|
218
|
+
const apiData = new Task(async (prev, abort) => {
|
|
219
|
+
const response = await fetch('/api/data', { signal: abort })
|
|
220
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
221
|
+
return response.json()
|
|
167
222
|
})
|
|
168
223
|
|
|
169
|
-
// Pattern matching for comprehensive state handling
|
|
170
224
|
createEffect(() => {
|
|
171
225
|
match(resolve({ apiData }), {
|
|
172
226
|
ok: ({ apiData }) => updateUI(apiData),
|
|
@@ -176,144 +230,70 @@ createEffect(() => {
|
|
|
176
230
|
})
|
|
177
231
|
```
|
|
178
232
|
|
|
179
|
-
### Performance Optimization
|
|
180
|
-
|
|
181
|
-
1. **Batching Updates**: Use `batch()` for multiple synchronous updates
|
|
182
|
-
2. **Selective Dependencies**: Structure computed signals to minimize dependencies
|
|
183
|
-
3. **Cleanup Management**: Properly dispose of effects to prevent memory leaks
|
|
184
|
-
4. **Shallow vs Deep Equality**: Use appropriate comparison strategies
|
|
233
|
+
### Performance Optimization
|
|
185
234
|
|
|
235
|
+
**Batching**: Use `batchSignalWrites()` for multiple updates
|
|
186
236
|
```typescript
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
id: 1,
|
|
190
|
-
name: 'John Doe',
|
|
191
|
-
email: 'john@example.com'
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
// Batch multiple updates to prevent intermediate states
|
|
195
|
-
batch(() => {
|
|
196
|
-
user.name.set('Alice Doe')
|
|
197
|
-
user.age.set(30)
|
|
237
|
+
batchSignalWrites(() => {
|
|
238
|
+
user.name.set('Alice')
|
|
198
239
|
user.email.set('alice@example.com')
|
|
199
|
-
}) //
|
|
200
|
-
|
|
201
|
-
// Optimize computed dependencies
|
|
202
|
-
const userDisplay = createComputed(() => {
|
|
203
|
-
const { name, email } = user.get() // Gets entire object
|
|
204
|
-
return `${name} <${email}>`
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
// Better: more granular dependencies
|
|
208
|
-
const userDisplay = createComputed(() => {
|
|
209
|
-
return `${user.name.get()} <${user.email.get()}>` // Only depends on name/email
|
|
210
|
-
})
|
|
240
|
+
}) // Single effect trigger
|
|
211
241
|
```
|
|
212
242
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- **Vanilla JS**: Direct DOM manipulation in effects
|
|
221
|
-
|
|
222
|
-
### Testing Strategies
|
|
223
|
-
- **Unit Testing**: Test signal logic in isolation
|
|
224
|
-
- **Integration Testing**: Test effect chains and async operations
|
|
225
|
-
- **Mock Testing**: Mock external dependencies in computed signals
|
|
226
|
-
- **Property Testing**: Use random inputs to verify invariants
|
|
227
|
-
|
|
228
|
-
### Debugging Techniques
|
|
229
|
-
- Use `resolve()` to inspect multiple signal states at once
|
|
230
|
-
- Add logging effects for debugging reactivity chains
|
|
231
|
-
- Use AbortSignal inspection for async operation debugging
|
|
232
|
-
- Leverage TypeScript for compile-time dependency analysis
|
|
243
|
+
**Granular Dependencies**: Structure computed signals to minimize dependencies
|
|
244
|
+
```typescript
|
|
245
|
+
// Bad: depends on entire user object
|
|
246
|
+
const display = new Memo(() => user.get().name + user.get().email)
|
|
247
|
+
// Good: only depends on specific properties
|
|
248
|
+
const display = new Memo(() => user.name.get() + user.email.get())
|
|
249
|
+
```
|
|
233
250
|
|
|
234
|
-
## Common Pitfalls
|
|
251
|
+
## Common Pitfalls
|
|
235
252
|
|
|
236
253
|
1. **Infinite Loops**: Don't update signals within their own computed callbacks
|
|
237
|
-
2. **
|
|
238
|
-
3. **
|
|
239
|
-
4. **
|
|
240
|
-
5. **Async Race Conditions**: Trust the automatic cancellation, don't fight it
|
|
254
|
+
2. **Memory Leaks**: Clean up effects when components unmount
|
|
255
|
+
3. **Over-reactivity**: Structure data to minimize unnecessary updates
|
|
256
|
+
4. **Async Race Conditions**: Trust automatic cancellation with AbortSignal
|
|
241
257
|
|
|
242
|
-
## Advanced
|
|
258
|
+
## Advanced Patterns
|
|
243
259
|
|
|
244
|
-
###
|
|
260
|
+
### Event Bus with Type Safety
|
|
245
261
|
```typescript
|
|
246
|
-
|
|
247
|
-
const todos = createStore<Todo[]>([])
|
|
248
|
-
const completedCount = createComputed(() =>
|
|
249
|
-
todos.get().filter(todo => todo.completed).length
|
|
250
|
-
)
|
|
251
|
-
const remainingCount = createComputed(() =>
|
|
252
|
-
todos.get().length - completedCount.get()
|
|
253
|
-
)
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Reactive State Machines
|
|
257
|
-
```typescript
|
|
258
|
-
const state = createState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
259
|
-
const canRetry = () => state.get() === 'error'
|
|
260
|
-
const isLoading = () => state.get() === 'loading'
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Cross-Component Communication
|
|
264
|
-
```typescript
|
|
265
|
-
// Component ownership pattern: The component that emits events owns the store
|
|
266
|
-
// Component B (the emitter) declares the shape of events it will emit upfront
|
|
267
|
-
type EventBusSchema = {
|
|
262
|
+
type Events = {
|
|
268
263
|
userLogin: { userId: number; timestamp: number }
|
|
269
264
|
userLogout: { userId: number }
|
|
270
|
-
userUpdate: { userId: number; profile: { name: string } }
|
|
271
265
|
}
|
|
272
266
|
|
|
273
|
-
|
|
274
|
-
const eventBus = createStore<EventBusSchema>({
|
|
267
|
+
const eventBus = createStore<Events>({
|
|
275
268
|
userLogin: UNSET,
|
|
276
|
-
userLogout: UNSET
|
|
277
|
-
userUpdate: UNSET,
|
|
269
|
+
userLogout: UNSET
|
|
278
270
|
})
|
|
279
271
|
|
|
280
|
-
|
|
281
|
-
const emit = <K extends keyof EventBusSchema>(
|
|
282
|
-
event: K,
|
|
283
|
-
data: EventBusSchema[K],
|
|
284
|
-
) => {
|
|
272
|
+
const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
|
|
285
273
|
eventBus[event].set(data)
|
|
286
274
|
}
|
|
287
275
|
|
|
288
|
-
|
|
289
|
-
const on = <K extends keyof EventBusSchema>(
|
|
290
|
-
event: K,
|
|
291
|
-
callback: (data: EventBusSchema[K]) => void,
|
|
292
|
-
) =>
|
|
276
|
+
const on = <K extends keyof Events>(event: K, callback: (data: Events[K]) => void) =>
|
|
293
277
|
createEffect(() => {
|
|
294
278
|
const data = eventBus[event].get()
|
|
295
279
|
if (data !== UNSET) callback(data)
|
|
296
280
|
})
|
|
297
|
-
|
|
298
|
-
// Usage example:
|
|
299
|
-
// Component A listens for user events
|
|
300
|
-
on('userLogin', (data) => {
|
|
301
|
-
console.log('User logged in:', data) // data is properly typed
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
// Component B emits user events
|
|
305
|
-
eventBus.userLogin.set({ userId: 123, timestamp: Date.now() })
|
|
306
281
|
```
|
|
307
282
|
|
|
308
|
-
|
|
283
|
+
### Data Processing Pipelines
|
|
284
|
+
```typescript
|
|
285
|
+
const rawData = new List([{ id: 1, value: 10 }])
|
|
286
|
+
const processed = rawData
|
|
287
|
+
.deriveCollection(item => ({ ...item, doubled: item.value * 2 }))
|
|
288
|
+
.deriveCollection(item => ({ ...item, formatted: `$${item.doubled}` }))
|
|
289
|
+
```
|
|
309
290
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
5. **Better performance**: Only the specific event listeners trigger, not all listeners
|
|
316
|
-
6. **Explicit dependencies**: Effects track exactly which events they care about
|
|
317
|
-
7. **IntelliSense support**: IDEs can provide autocomplete for event names and data structures
|
|
291
|
+
### Stable List Keys
|
|
292
|
+
```typescript
|
|
293
|
+
const playlist = new List([
|
|
294
|
+
{ id: 'track1', title: 'Song A' }
|
|
295
|
+
], track => track.id)
|
|
318
296
|
|
|
319
|
-
|
|
297
|
+
const firstTrack = playlist.byKey('track1') // Persists through sorting
|
|
298
|
+
playlist.sort((a, b) => a.title.localeCompare(b.title))
|
|
299
|
+
```
|
package/LICENSE
CHANGED