@owlmeans/state 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,456 @@
1
+ # @owlmeans/state
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.
4
+
5
+ ## Overview
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
16
+
17
+ ## Installation
18
+
19
+ ```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
+ }
54
+ ```
55
+
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
99
+
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
152
+
153
+ ```typescript
154
+ import { createStateResource } from '@owlmeans/state'
155
+
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()
186
+ ```
187
+
188
+ ### Multiple Record Subscription
189
+
190
+ ```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
+ ```
202
+
203
+ ### Query-Based Subscription
204
+
205
+ ```typescript
206
+ // Subscribe to active users
207
+ const [unsubscribe, models] = userState.subscribe({
208
+ query: {
209
+ status: 'active',
210
+ role: ['admin', 'moderator']
211
+ },
212
+ listener: (models) => {
213
+ console.log(`Active admin/moderator users: ${models.length}`)
214
+ }
215
+ })
216
+ ```
217
+
218
+ ### Global State Listening
219
+
220
+ ```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
+ })
226
+ ```
227
+
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)
235
+
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
+ })
244
+
245
+ // Set initial state
246
+ if (models.length > 0) {
247
+ setUser(models[0])
248
+ }
249
+
250
+ return unsubscribe
251
+ }, [userId])
252
+
253
+ const handleUpdate = (data: Partial<User>) => {
254
+ if (user) {
255
+ user.update(data)
256
+ user.commit()
257
+ }
258
+ }
259
+
260
+ if (!user) {
261
+ return <div>Loading...</div>
262
+ }
263
+
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
+ ```
276
+
277
+ ### Context Integration
278
+
279
+ ```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')
289
+ ```
290
+
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
455
+
456
+ Fixes #32.
package/build/.gitkeep ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_ID = "_default";
2
+ export declare const DEFAULT_ALIAS = "state";
3
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,aAAa,CAAA;AACpC,eAAO,MAAM,aAAa,UAAU,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_ID = '_default';
2
+ export const DEFAULT_ALIAS = 'state';
3
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAA;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { ResourceError } from '@owlmeans/resource';
2
+ export declare class StateToolingError extends ResourceError {
3
+ static typeName: string;
4
+ constructor(msg: string);
5
+ }
6
+ export declare class StateListenerError extends StateToolingError {
7
+ static typeName: string;
8
+ constructor(msg: string);
9
+ }
10
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,qBAAa,iBAAkB,SAAQ,aAAa;IAClD,OAAuB,QAAQ,SAAqC;gBAExD,GAAG,EAAE,MAAM;CAIxB;AAED,qBAAa,kBAAmB,SAAQ,iBAAiB;IACvD,OAAuB,QAAQ,SAA0C;gBAE7D,GAAG,EAAE,MAAM;CAIxB"}
@@ -0,0 +1,18 @@
1
+ import { ResourceError } from '@owlmeans/resource';
2
+ export class StateToolingError extends ResourceError {
3
+ static typeName = `${ResourceError.typeName}Tooling`;
4
+ constructor(msg) {
5
+ super(`tooling:${msg}`);
6
+ this.type = StateToolingError.typeName;
7
+ }
8
+ }
9
+ export class StateListenerError extends StateToolingError {
10
+ static typeName = `${StateToolingError.typeName}Listener`;
11
+ constructor(msg) {
12
+ super(`listener:${msg}`);
13
+ this.type = StateListenerError.typeName;
14
+ }
15
+ }
16
+ ResourceError.registerErrorClass(StateToolingError);
17
+ ResourceError.registerErrorClass(StateListenerError);
18
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,MAAM,OAAO,iBAAkB,SAAQ,aAAa;IAC3C,MAAM,CAAU,QAAQ,GAAG,GAAG,aAAa,CAAC,QAAQ,SAAS,CAAA;IAEpE,YAAY,GAAW;QACrB,KAAK,CAAC,WAAW,GAAG,EAAE,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC,QAAQ,CAAA;IACxC,CAAC;;AAGH,MAAM,OAAO,kBAAmB,SAAQ,iBAAiB;IAChD,MAAM,CAAU,QAAQ,GAAG,GAAG,iBAAiB,CAAC,QAAQ,UAAU,CAAA;IAEzE,YAAY,GAAW;QACrB,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;;AAGH,aAAa,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAA;AACnD,aAAa,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA"}
@@ -0,0 +1,5 @@
1
+ export type * from './types.js';
2
+ export * from './consts.js';
3
+ export * from './errors.js';
4
+ export * from './resource.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './consts.js';
2
+ export * from './errors.js';
3
+ export * from './resource.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,6 @@
1
+ import type { ResourceRecord } from '@owlmeans/resource';
2
+ import type { StateResource, StateResourceAppend } from './types.js';
3
+ import type { BasicContext as Context, BasicConfig as Config } from '@owlmeans/context';
4
+ export declare const createStateResource: <R extends ResourceRecord>(alias?: string) => StateResource<R>;
5
+ export declare const appendStateResource: <C extends Config, T extends Context<C>>(ctx: T, alias?: string) => T & StateResourceAppend;
6
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgC,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACtF,OAAO,KAAK,EAAiB,aAAa,EAAE,mBAAmB,EAA2B,MAAM,YAAY,CAAA;AAI5G,OAAO,KAAK,EAAE,YAAY,IAAI,OAAO,EAAE,WAAW,IAAI,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEvF,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,cAAc,UAAS,MAAM,KAAmB,aAAa,CAAC,CAAC,CAuO5G,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OACnE,CAAC,UAAS,MAAM,KACpB,CAAC,GAAG,mBAWN,CAAA"}