@owlmeans/state 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +39 -413
  2. package/package.json +5 -4
  3. package/tsconfig.json +5 -9
package/README.md CHANGED
@@ -1,456 +1,82 @@
1
1
  # @owlmeans/state
2
2
 
3
- A reactive state management library for OwlMeans Common applications. This package provides a comprehensive state management system with subscription-based reactivity, resource integration, and real-time data synchronization for building responsive fullstack applications.
3
+ In-memory reactive state management with subscription-based updates for OwlMeans apps.
4
4
 
5
5
  ## Overview
6
6
 
7
- The `@owlmeans/state` package is a reactive state management library in the OwlMeans Common ecosystem that provides:
8
-
9
- - **Reactive State Management**: Subscribe to state changes with automatic updates
10
- - **Resource Integration**: Built on top of OwlMeans Resource system for data persistence
11
- - **Real-time Synchronization**: Live updates across multiple subscribers
12
- - **Model-Based Architecture**: Type-safe state models with built-in update methods
13
- - **Query Subscriptions**: Subscribe to filtered data sets with criteria
14
- - **Memory Management**: Automatic cleanup and unsubscription handling
15
- - **Context Integration**: Seamless integration with OwlMeans context system
7
+ - `createStateResource()` creates an in-memory store that implements the `Resource<T>` interface
8
+ - Subscriptions trigger listeners whenever records are created, updated, or deleted
9
+ - Used on the client to hold UI state (projects, stories, thinking journal entries) as reactive records
10
+ - `DEFAULT_ID` (`'_default'`) is the conventional ID for single-record resources
16
11
 
17
12
  ## Installation
18
13
 
19
14
  ```bash
20
- npm install @owlmeans/state
21
- ```
22
-
23
- ## Core Concepts
24
-
25
- ### State Resource
26
-
27
- The `StateResource` extends the standard OwlMeans Resource interface with reactive capabilities, allowing components to subscribe to data changes and receive automatic updates.
28
-
29
- ### State Model
30
-
31
- A `StateModel` wraps resource records with reactive capabilities, providing methods to update data and automatically notify subscribers of changes.
32
-
33
- ### Subscriptions
34
-
35
- Subscriptions connect components to state changes, automatically triggering updates when subscribed data changes.
36
-
37
- ### Listeners
38
-
39
- Functions that handle state change notifications, receiving updated state models when data changes.
40
-
41
- ## API Reference
42
-
43
- ### Types
44
-
45
- #### `StateResource<T extends ResourceRecord>`
46
- Extended resource interface with subscription capabilities.
47
-
48
- ```typescript
49
- interface StateResource<T extends ResourceRecord> extends Resource<T> {
50
- subscribe: (params: StateSubscriptionOption<T>) => [() => void, StateModel<T>[]]
51
- listen: (listener: StateListener<T>) => () => void
52
- erase: () => Promise<void>
53
- }
15
+ bun add @owlmeans/state
54
16
  ```
55
17
 
56
- #### `StateModel<T extends ResourceRecord>`
57
- Reactive wrapper for resource records.
58
-
59
- ```typescript
60
- interface StateModel<T extends ResourceRecord> {
61
- record: T
62
- commit: (force?: boolean) => void
63
- update: (data?: Partial<T>) => void
64
- clear: () => void
65
- }
66
- ```
67
-
68
- #### `StateSubscriptionOption<T>`
69
- Configuration for state subscriptions.
70
-
71
- ```typescript
72
- interface StateSubscriptionOption<T extends ResourceRecord> {
73
- id?: string | string[] // Specific record IDs to subscribe to
74
- _systemId?: string // System-level subscription identifier
75
- query?: ListCriteria // Query criteria for filtered subscriptions
76
- default?: Partial<T> // Default data for new records
77
- listener: StateListener<T> // Callback function for updates
78
- }
79
- ```
80
-
81
- #### `StateListener<T>`
82
- Function type for handling state changes.
83
-
84
- ```typescript
85
- interface StateListener<T extends ResourceRecord> {
86
- (record: StateModel<T>[]): void | Promise<void>
87
- }
88
- ```
89
-
90
- ### Factory Functions
91
-
92
- #### `createStateResource<R>(alias?: string): StateResource<R>`
93
- Creates a new state resource with reactive capabilities.
94
-
95
- **Parameters:**
96
- - `alias`: Optional resource alias (defaults to 'state')
97
-
98
- **Returns:** StateResource instance with subscription support
18
+ ## Usage
99
19
 
100
- #### `appendStateResource<C, T>(ctx: T, alias?: string): T & StateResourceAppend`
101
- Appends a state resource to an existing context.
102
-
103
- **Parameters:**
104
- - `ctx`: OwlMeans context instance
105
- - `alias`: Optional resource alias
106
-
107
- **Returns:** Context with state resource capabilities
108
-
109
- ### Core Methods
110
-
111
- #### `subscribe(params: StateSubscriptionOption<T>): [() => void, StateModel<T>[]]`
112
- Subscribes to state changes for specific records or queries.
113
-
114
- **Parameters:**
115
- - `params`: Subscription configuration with listener and criteria
116
-
117
- **Returns:** Tuple of [unsubscribe function, current state models]
118
-
119
- #### `listen(listener: StateListener<T>): () => void`
120
- Adds a global listener for all state changes.
121
-
122
- **Parameters:**
123
- - `listener`: Function to handle state changes
124
-
125
- **Returns:** Unsubscribe function
126
-
127
- #### `erase(): Promise<void>`
128
- Clears all state data and notifies subscribers.
129
-
130
- ### State Model Methods
131
-
132
- #### `commit(force?: boolean): void`
133
- Persists model changes to the underlying resource.
134
-
135
- #### `update(data?: Partial<T>): void`
136
- Updates model data and notifies subscribers.
137
-
138
- #### `clear(): void`
139
- Clears model data while maintaining subscription.
140
-
141
- ### Error Types
142
-
143
- #### `StateToolingError`
144
- Base error for state management tooling issues.
145
-
146
- #### `StateListenerError`
147
- Error related to listener management and execution.
148
-
149
- ## Usage Examples
150
-
151
- ### Basic State Subscription
20
+ Create and register a state resource in context setup:
152
21
 
153
22
  ```typescript
154
23
  import { createStateResource } from '@owlmeans/state'
155
24
 
156
- interface User extends ResourceRecord {
157
- id?: string
158
- name: string
159
- email: string
160
- status: 'active' | 'inactive'
161
- }
162
-
163
- // Create state resource
164
- const userState = createStateResource<User>('users')
165
-
166
- // Subscribe to a specific user
167
- const [unsubscribe, models] = userState.subscribe({
168
- id: 'user123',
169
- listener: (models) => {
170
- const user = models[0]
171
- console.log('User updated:', user.record)
172
- }
173
- })
174
-
175
- // Get current state immediately
176
- if (models.length > 0) {
177
- console.log('Current user:', models[0].record)
178
- }
179
-
180
- // Update user data
181
- models[0].update({ status: 'inactive' })
182
- models[0].commit()
183
-
184
- // Cleanup
185
- unsubscribe()
25
+ // In context.ts
26
+ context.registerResource(createStateResource<ProjectState>('project-state'))
186
27
  ```
187
28
 
188
- ### Multiple Record Subscription
29
+ Subscribe to state changes in a service or component:
189
30
 
190
31
  ```typescript
191
- // Subscribe to multiple users
192
- const [unsubscribe, models] = userState.subscribe({
193
- id: ['user1', 'user2', 'user3'],
194
- listener: (models) => {
195
- console.log(`Received updates for ${models.length} users`)
196
- models.forEach(model => {
197
- console.log(`User ${model.record.id}: ${model.record.name}`)
198
- })
199
- }
200
- })
201
- ```
32
+ import { DEFAULT_ID } from '@owlmeans/state'
33
+ import type { StateModel, StateResource } from '@owlmeans/state'
202
34
 
203
- ### Query-Based Subscription
35
+ const resource = context.resource<StateResource<ProjectState>>('project-state')
204
36
 
205
- ```typescript
206
- // Subscribe to active users
207
- const [unsubscribe, models] = userState.subscribe({
208
- query: {
209
- status: 'active',
210
- role: ['admin', 'moderator']
211
- },
37
+ const [unsubscribe] = resource.subscribe({
38
+ id: DEFAULT_ID,
212
39
  listener: (models) => {
213
- console.log(`Active admin/moderator users: ${models.length}`)
40
+ const [model] = models
41
+ console.log('project updated:', model.record)
214
42
  }
215
43
  })
216
44
  ```
217
45
 
218
- ### Global State Listening
46
+ Update state and commit:
219
47
 
220
48
  ```typescript
221
- // Listen to all state changes
222
- const unsubscribe = userState.listen((models) => {
223
- console.log('Global state change detected')
224
- // Handle any user state changes
225
- })
49
+ const [model] = resource.subscribe({ id: projectId, listener: ... })[1]
50
+ model.update({ status: 'active' })
51
+ model.commit() // triggers listeners
226
52
  ```
227
53
 
228
- ### React Integration Example
229
-
230
- ```typescript
231
- import React, { useEffect, useState } from 'react'
232
-
233
- const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
234
- const [user, setUser] = useState<StateModel<User> | null>(null)
54
+ ## API
235
55
 
236
- useEffect(() => {
237
- const [unsubscribe, models] = userState.subscribe({
238
- id: userId,
239
- default: { name: '', email: '', status: 'active' },
240
- listener: (models) => {
241
- setUser(models[0] || null)
242
- }
243
- })
56
+ ### `createStateResource<T>(alias?): StateResource<T>`
244
57
 
245
- // Set initial state
246
- if (models.length > 0) {
247
- setUser(models[0])
248
- }
58
+ Creates an in-memory resource with subscription support. Registers under `alias` (default: `'state'`).
249
59
 
250
- return unsubscribe
251
- }, [userId])
60
+ ### `StateResource<T>` (extends `Resource<T>`)
252
61
 
253
- const handleUpdate = (data: Partial<User>) => {
254
- if (user) {
255
- user.update(data)
256
- user.commit()
257
- }
258
- }
62
+ - `subscribe(params): [unsubscribe, StateModel<T>[]]` subscribe to record changes; returns current records
63
+ - `listen(listener)` — global listener for any change in the resource
64
+ - `erase()` — clear all records
259
65
 
260
- if (!user) {
261
- return <div>Loading...</div>
262
- }
66
+ ### `StateModel<T>`
263
67
 
264
- return (
265
- <div>
266
- <h2>{user.record.name}</h2>
267
- <p>Email: {user.record.email}</p>
268
- <p>Status: {user.record.status}</p>
269
- <button onClick={() => handleUpdate({ status: 'inactive' })}>
270
- Deactivate User
271
- </button>
272
- </div>
273
- )
274
- }
275
- ```
68
+ - `record: T` — the current record
69
+ - `update(data?)` — merge partial data into the record
70
+ - `commit(force?)` — apply changes and notify subscribers
71
+ - `clear()` — remove the record
276
72
 
277
- ### Context Integration
73
+ ### `DEFAULT_ID`
278
74
 
279
75
  ```typescript
280
- import { createBasicContext } from '@owlmeans/context'
281
- import { appendStateResource } from '@owlmeans/state'
282
-
283
- // Create context with state capabilities
284
- const context = createBasicContext()
285
- appendStateResource(context, 'user-state')
286
-
287
- // Access state resource from context
288
- const stateResource = context.getStateResource<User>('user-state')
76
+ const DEFAULT_ID = '_default' // conventional ID for single-item resources
289
77
  ```
290
78
 
291
- ### Bulk Operations
292
-
293
- ```typescript
294
- // Subscribe to system-level changes
295
- const [unsubscribe, models] = userState.subscribe({
296
- _systemId: 'bulk-operations',
297
- listener: async (models) => {
298
- // Handle bulk updates
299
- console.log(`Processing ${models.length} records`)
300
-
301
- // Batch commit changes
302
- models.forEach(model => model.commit())
303
- }
304
- })
305
-
306
- // Perform bulk updates
307
- await userState.create({ name: 'User 1', email: 'user1@example.com' })
308
- await userState.create({ name: 'User 2', email: 'user2@example.com' })
309
- ```
310
-
311
- ### Default Data Handling
312
-
313
- ```typescript
314
- // Subscribe with default data for new records
315
- const [unsubscribe, models] = userState.subscribe({
316
- id: 'new-user',
317
- default: {
318
- name: 'New User',
319
- email: '',
320
- status: 'active'
321
- },
322
- listener: (models) => {
323
- const user = models[0]
324
- if (!user.record.email) {
325
- // User still needs email
326
- console.log('User needs email address')
327
- }
328
- }
329
- })
330
- ```
331
-
332
- ### Error Handling
333
-
334
- ```typescript
335
- import { StateListenerError } from '@owlmeans/state'
336
-
337
- try {
338
- const [unsubscribe, models] = userState.subscribe({
339
- id: 'user123',
340
- listener: async (models) => {
341
- // Async listener that might throw
342
- await processUserData(models[0].record)
343
- }
344
- })
345
- } catch (error) {
346
- if (error instanceof StateListenerError) {
347
- console.error('Listener error:', error.message)
348
- }
349
- }
350
- ```
351
-
352
- ## Advanced Features
353
-
354
- ### Custom Subscription Patterns
355
-
356
- ```typescript
357
- // Conditional subscriptions
358
- const subscribeToUser = (userId: string, condition: (user: User) => boolean) => {
359
- return userState.subscribe({
360
- id: userId,
361
- listener: (models) => {
362
- const user = models[0]
363
- if (user && condition(user.record)) {
364
- // Handle conditional updates
365
- console.log('Condition met for user:', user.record.id)
366
- }
367
- }
368
- })
369
- }
370
-
371
- // Subscribe only to active users
372
- const [unsubscribe] = subscribeToUser('user123', user => user.status === 'active')
373
- ```
374
-
375
- ### Memory Management
376
-
377
- ```typescript
378
- class UserManager {
379
- private subscriptions: (() => void)[] = []
380
-
381
- subscribeToUser(userId: string) {
382
- const [unsubscribe] = userState.subscribe({
383
- id: userId,
384
- listener: this.handleUserUpdate.bind(this)
385
- })
386
-
387
- this.subscriptions.push(unsubscribe)
388
- }
389
-
390
- cleanup() {
391
- // Clean up all subscriptions
392
- this.subscriptions.forEach(unsubscribe => unsubscribe())
393
- this.subscriptions = []
394
- }
395
-
396
- private handleUserUpdate(models: StateModel<User>[]) {
397
- // Handle updates
398
- }
399
- }
400
- ```
401
-
402
- ### Performance Optimization
403
-
404
- ```typescript
405
- // Debounced updates
406
- let updateTimeout: NodeJS.Timeout
407
-
408
- const [unsubscribe] = userState.subscribe({
409
- id: 'user123',
410
- listener: (models) => {
411
- clearTimeout(updateTimeout)
412
- updateTimeout = setTimeout(() => {
413
- // Process updates after debounce
414
- console.log('Processing debounced update')
415
- }, 100)
416
- }
417
- })
418
- ```
419
-
420
- ## Integration with OwlMeans Ecosystem
421
-
422
- The `@owlmeans/state` package integrates with:
423
-
424
- - **@owlmeans/resource**: Built on the resource system for data persistence
425
- - **@owlmeans/context**: Context-based service registration and access
426
- - **@owlmeans/client**: React hooks and component integration
427
- - **@owlmeans/error**: Error handling and reporting
428
- - **@owlmeans/client-resource**: Client-side resource management
429
-
430
- ## Best Practices
431
-
432
- ### Subscription Management
433
- - Always store unsubscribe functions and call them during cleanup
434
- - Use specific IDs when possible instead of broad queries
435
- - Implement proper error handling in listeners
436
- - Avoid creating subscriptions in render loops
437
-
438
- ### Performance
439
- - Use query-based subscriptions for filtered data sets
440
- - Implement debouncing for high-frequency updates
441
- - Clean up unused subscriptions promptly
442
- - Use default data to avoid loading states
443
-
444
- ### Memory Management
445
- - Implement proper cleanup in component unmount
446
- - Use weak references for large object graphs
447
- - Monitor subscription count in development
448
- - Clean up system subscriptions when no longer needed
449
-
450
- ### Error Handling
451
- - Wrap async listeners in try-catch blocks
452
- - Implement fallback UI for failed state updates
453
- - Log subscription errors for debugging
454
- - Use defensive programming for model access
79
+ ## Related Packages
455
80
 
456
- Fixes #32.
81
+ - [`@owlmeans/resource`](../resource) — `Resource<T>` interface implemented by StateResource
82
+ - [`@owlmeans/client`](../client) — `useStoreModel` / `useStoreList` React hooks for state resources
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/state",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,12 +21,13 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@owlmeans/context": "^0.1.2",
25
- "@owlmeans/resource": "^0.1.2"
24
+ "@owlmeans/context": "^0.1.4",
25
+ "@owlmeans/resource": "^0.1.4"
26
26
  },
27
27
  "devDependencies": {
28
+ "@owlmeans/dep-config": "workspace:*",
28
29
  "nodemon": "^3.1.11",
29
- "typescript": "^5.8.3"
30
+ "typescript": "^6.0.2"
30
31
  },
31
32
  "publishConfig": {
32
33
  "access": "public"
package/tsconfig.json CHANGED
@@ -1,14 +1,10 @@
1
1
  {
2
2
  "extends": [
3
- "../tsconfig.default.json"
3
+ "@owlmeans/dep-config/tsconfig.base.json"
4
4
  ],
5
5
  "compilerOptions": {
6
- "rootDir": "./src/", /* Specify the root folder within your source files. */
7
- "outDir": "./build/", /* Specify an output folder for all emitted files. */
6
+ "rootDir": "./src/",
7
+ "outDir": "./build/"
8
8
  },
9
- "exclude": [
10
- "./dist/**/*",
11
- "./build/**/*",
12
- "./*.ts"
13
- ]
14
- }
9
+ "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
10
+ }