@stonecrop/stonecrop 0.4.37 → 0.6.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.
Files changed (78) hide show
  1. package/README.md +92 -3
  2. package/dist/src/composable.d.ts +74 -8
  3. package/dist/src/composable.d.ts.map +1 -1
  4. package/dist/src/composable.js +348 -0
  5. package/dist/src/composables/operation-log.d.ts +136 -0
  6. package/dist/src/composables/operation-log.d.ts.map +1 -0
  7. package/dist/src/composables/operation-log.js +221 -0
  8. package/dist/src/doctype.d.ts +9 -1
  9. package/dist/src/doctype.d.ts.map +1 -1
  10. package/dist/{doctype.js → src/doctype.js} +9 -3
  11. package/dist/src/field-triggers.d.ts +178 -0
  12. package/dist/src/field-triggers.d.ts.map +1 -0
  13. package/dist/src/field-triggers.js +564 -0
  14. package/dist/src/index.d.ts +12 -4
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +18 -0
  17. package/dist/src/plugins/index.d.ts +11 -13
  18. package/dist/src/plugins/index.d.ts.map +1 -1
  19. package/dist/src/plugins/index.js +90 -0
  20. package/dist/src/registry.d.ts +9 -3
  21. package/dist/src/registry.d.ts.map +1 -1
  22. package/dist/{registry.js → src/registry.js} +14 -1
  23. package/dist/src/stonecrop.d.ts +350 -114
  24. package/dist/src/stonecrop.d.ts.map +1 -1
  25. package/dist/src/stonecrop.js +251 -0
  26. package/dist/src/stores/hst.d.ts +157 -0
  27. package/dist/src/stores/hst.d.ts.map +1 -0
  28. package/dist/src/stores/hst.js +483 -0
  29. package/dist/src/stores/index.d.ts +5 -1
  30. package/dist/src/stores/index.d.ts.map +1 -1
  31. package/dist/{stores → src/stores}/index.js +4 -1
  32. package/dist/src/stores/operation-log.d.ts +268 -0
  33. package/dist/src/stores/operation-log.d.ts.map +1 -0
  34. package/dist/src/stores/operation-log.js +571 -0
  35. package/dist/src/types/field-triggers.d.ts +186 -0
  36. package/dist/src/types/field-triggers.d.ts.map +1 -0
  37. package/dist/src/types/field-triggers.js +4 -0
  38. package/dist/src/types/index.d.ts +13 -2
  39. package/dist/src/types/index.d.ts.map +1 -1
  40. package/dist/src/types/index.js +4 -0
  41. package/dist/src/types/operation-log.d.ts +165 -0
  42. package/dist/src/types/operation-log.d.ts.map +1 -0
  43. package/dist/src/types/registry.d.ts +11 -0
  44. package/dist/src/types/registry.d.ts.map +1 -0
  45. package/dist/src/types/registry.js +0 -0
  46. package/dist/stonecrop.d.ts +1555 -159
  47. package/dist/stonecrop.js +1974 -7028
  48. package/dist/stonecrop.js.map +1 -1
  49. package/dist/stonecrop.umd.cjs +4 -8
  50. package/dist/stonecrop.umd.cjs.map +1 -1
  51. package/dist/tests/setup.d.ts +5 -0
  52. package/dist/tests/setup.d.ts.map +1 -0
  53. package/dist/tests/setup.js +15 -0
  54. package/package.json +6 -5
  55. package/src/composable.ts +481 -31
  56. package/src/composables/operation-log.ts +254 -0
  57. package/src/doctype.ts +9 -3
  58. package/src/field-triggers.ts +671 -0
  59. package/src/index.ts +50 -4
  60. package/src/plugins/index.ts +70 -22
  61. package/src/registry.ts +18 -3
  62. package/src/stonecrop.ts +246 -155
  63. package/src/stores/hst.ts +703 -0
  64. package/src/stores/index.ts +6 -1
  65. package/src/stores/operation-log.ts +671 -0
  66. package/src/types/field-triggers.ts +201 -0
  67. package/src/types/index.ts +17 -6
  68. package/src/types/operation-log.ts +205 -0
  69. package/src/types/registry.ts +10 -0
  70. package/dist/composable.js +0 -50
  71. package/dist/index.js +0 -6
  72. package/dist/plugins/index.js +0 -49
  73. package/dist/src/stores/data.d.ts +0 -11
  74. package/dist/src/stores/data.d.ts.map +0 -1
  75. package/dist/stores/data.js +0 -7
  76. package/src/stores/data.ts +0 -8
  77. /package/dist/{exceptions.js → src/exceptions.js} +0 -0
  78. /package/dist/{types/index.js → src/types/operation-log.js} +0 -0
@@ -0,0 +1,703 @@
1
+ import { getGlobalTriggerEngine } from '../field-triggers'
2
+ import type { FieldChangeContext, TransitionChangeContext } from '../types/field-triggers'
3
+ import { useOperationLogStore } from './operation-log'
4
+
5
+ /**
6
+ * Get the operation log store if available
7
+ */
8
+ function getOperationLogStore() {
9
+ try {
10
+ return useOperationLogStore()
11
+ } catch {
12
+ // Operation log is optional
13
+ return null
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Core HST Interface - enhanced with tree navigation
19
+ * Provides a hierarchical state tree interface for navigating and manipulating nested data structures.
20
+ *
21
+ * @public
22
+ */
23
+ interface HSTNode {
24
+ /**
25
+ * Gets a value at the specified path
26
+ * @param path - The dot-separated path to the value
27
+ * @returns The value at the specified path
28
+ */
29
+ get(path: string): any
30
+
31
+ /**
32
+ * Sets a value at the specified path
33
+ * @param path - The dot-separated path where to set the value
34
+ * @param value - The value to set
35
+ * @param source - Optional source of the operation (user, system, sync, undo, redo)
36
+ */
37
+ set(path: string, value: any, source?: 'user' | 'system' | 'sync' | 'undo' | 'redo'): void
38
+
39
+ /**
40
+ * Checks if a value exists at the specified path
41
+ * @param path - The dot-separated path to check
42
+ * @returns True if the path exists, false otherwise
43
+ */
44
+ has(path: string): boolean
45
+
46
+ /**
47
+ * Gets the parent node in the tree hierarchy
48
+ * @returns The parent HSTNode or null if this is the root
49
+ */
50
+ getParent(): HSTNode | null
51
+
52
+ /**
53
+ * Gets the root node of the tree
54
+ * @returns The root HSTNode
55
+ */
56
+ getRoot(): HSTNode
57
+
58
+ /**
59
+ * Gets the full path from root to this node
60
+ * @returns The dot-separated path string
61
+ */
62
+ getPath(): string
63
+
64
+ /**
65
+ * Gets the depth level of this node in the tree
66
+ * @returns The depth as a number (0 for root)
67
+ */
68
+ getDepth(): number
69
+
70
+ /**
71
+ * Gets an array of path segments from root to this node
72
+ * @returns Array of path segments representing breadcrumbs
73
+ */
74
+ getBreadcrumbs(): string[]
75
+
76
+ /**
77
+ * Gets a child node at the specified relative path
78
+ * @param path - The relative path to the child node
79
+ * @returns The child HSTNode
80
+ */
81
+ getNode(path: string): HSTNode
82
+
83
+ /**
84
+ * Trigger an XState transition with optional context data
85
+ * @param transition - The transition name (should be uppercase per convention)
86
+ * @param context - Optional additional FSM context data
87
+ * @returns Promise resolving to the transition execution results
88
+ */
89
+ triggerTransition(
90
+ transition: string,
91
+ context?: { currentState?: string; targetState?: string; fsmContext?: Record<string, any> }
92
+ ): Promise<any>
93
+ }
94
+
95
+ // Type definitions for global Registry
96
+ interface RegistryGlobal {
97
+ Registry?: {
98
+ _root?: {
99
+ registry: Record<string, any>
100
+ }
101
+ }
102
+ }
103
+
104
+ // Interface for Immutable-like objects
105
+ interface ImmutableLike {
106
+ get(key: string): any
107
+ set(key: string, value: any): ImmutableLike
108
+ has(key: string): boolean
109
+ size?: number
110
+ __ownerID?: any
111
+ _map?: any
112
+ _list?: any
113
+ _origin?: any
114
+ _capacity?: any
115
+ _defaultValues?: any
116
+ _tail?: any
117
+ _root?: any
118
+ }
119
+
120
+ // Interface for Vue reactive objects
121
+ interface VueReactive {
122
+ __v_isReactive: boolean
123
+ [key: string]: any
124
+ }
125
+
126
+ // Interface for Pinia stores
127
+ interface PiniaStore {
128
+ $state?: Record<string, any>
129
+ $patch?: (partial: Record<string, any>) => void
130
+ $id?: string
131
+ [key: string]: any
132
+ }
133
+
134
+ // Interface for objects with property access
135
+ interface PropertyAccessible {
136
+ [key: string]: any
137
+ }
138
+
139
+ // Extend global interfaces
140
+ declare global {
141
+ interface Window extends RegistryGlobal {}
142
+ const global: RegistryGlobal | undefined
143
+ }
144
+
145
+ /**
146
+ * Global HST Manager (Singleton)
147
+ * Manages hierarchical state trees and provides access to the global registry.
148
+ *
149
+ * @public
150
+ */
151
+ class HST {
152
+ private static instance: HST
153
+
154
+ /**
155
+ * Gets the singleton instance of HST
156
+ * @returns The HST singleton instance
157
+ */
158
+ static getInstance(): HST {
159
+ if (!HST.instance) {
160
+ HST.instance = new HST()
161
+ }
162
+ return HST.instance
163
+ }
164
+
165
+ /**
166
+ * Gets the global registry instance
167
+ * @returns The global registry object or undefined if not found
168
+ */
169
+ getRegistry(): any {
170
+ // In test environment, try different ways to access Registry
171
+ // First, try the global Registry if it exists
172
+ if (typeof globalThis !== 'undefined') {
173
+ const globalRegistry = (globalThis as RegistryGlobal).Registry?._root
174
+ if (globalRegistry) {
175
+ return globalRegistry
176
+ }
177
+ }
178
+
179
+ // Try to access through window (browser environment)
180
+ if (typeof window !== 'undefined') {
181
+ const windowRegistry = window.Registry?._root
182
+ if (windowRegistry) {
183
+ return windowRegistry
184
+ }
185
+ }
186
+
187
+ // Try to access through global (Node environment)
188
+ if (typeof global !== 'undefined' && global) {
189
+ const nodeRegistry = global.Registry?._root
190
+ if (nodeRegistry) {
191
+ return nodeRegistry
192
+ }
193
+ }
194
+
195
+ // If we can't find it globally, it might not be set up
196
+ // This is expected in test environments where Registry is created locally
197
+ return undefined
198
+ }
199
+
200
+ /**
201
+ * Helper method to get doctype metadata from the registry
202
+ * @param doctype - The name of the doctype to retrieve metadata for
203
+ * @returns The doctype metadata object or undefined if not found
204
+ */
205
+ getDoctypeMeta(doctype: string) {
206
+ const registry = this.getRegistry()
207
+ if (registry && typeof registry === 'object' && 'registry' in registry) {
208
+ return (registry as { registry: Record<string, any> }).registry[doctype]
209
+ }
210
+ return undefined
211
+ }
212
+ }
213
+
214
+ // Enhanced HST Proxy with tree navigation
215
+ class HSTProxy implements HSTNode {
216
+ private target: any
217
+ private parentPath: string
218
+ private rootNode: HSTNode | null
219
+ private doctype: string
220
+ private parentDoctype?: string
221
+ private hst: HST
222
+
223
+ constructor(target: any, doctype: string, parentPath = '', rootNode: HSTNode | null = null, parentDoctype?: string) {
224
+ this.target = target
225
+ this.parentPath = parentPath
226
+ this.rootNode = rootNode || this
227
+ this.doctype = doctype
228
+ this.parentDoctype = parentDoctype
229
+ this.hst = HST.getInstance()
230
+
231
+ return new Proxy(this, {
232
+ get(hst, prop) {
233
+ // Return HST methods directly
234
+ if (prop in hst) return hst[prop]
235
+
236
+ // Handle property access - return tree nodes for navigation
237
+ const path = String(prop)
238
+ return hst.getNode(path)
239
+ },
240
+
241
+ set(hst, prop, value) {
242
+ const path = String(prop)
243
+ hst.set(path, value)
244
+ return true
245
+ },
246
+ })
247
+ }
248
+
249
+ get(path: string): any {
250
+ return this.resolveValue(path)
251
+ }
252
+
253
+ // Method to get a tree-wrapped node for navigation
254
+ getNode(path: string): HSTNode {
255
+ const fullPath = this.resolvePath(path)
256
+ const value = this.resolveValue(path)
257
+
258
+ // Determine the correct doctype for this node based on the path
259
+ const pathSegments = fullPath.split('.')
260
+ let nodeDoctype = this.doctype
261
+
262
+ // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
263
+ if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
264
+ nodeDoctype = pathSegments[0]
265
+ }
266
+
267
+ // Always wrap in HSTProxy for tree navigation
268
+ if (typeof value === 'object' && value !== null && !this.isPrimitive(value)) {
269
+ return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode, this.parentDoctype)
270
+ }
271
+
272
+ // For primitives, return a minimal wrapper that throws on tree operations
273
+ return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode, this.parentDoctype)
274
+ }
275
+
276
+ set(path: string, value: any, source: 'user' | 'system' | 'sync' | 'undo' | 'redo' = 'user'): void {
277
+ // Get current value for change context
278
+ const fullPath = this.resolvePath(path)
279
+ const beforeValue = this.has(path) ? this.get(path) : undefined
280
+
281
+ // Log operation if not from undo/redo and store is available
282
+ if (source !== 'undo' && source !== 'redo') {
283
+ const logStore = getOperationLogStore()
284
+ if (logStore && typeof logStore.addOperation === 'function') {
285
+ const pathSegments = fullPath.split('.')
286
+ const doctype = this.doctype === 'StonecropStore' && pathSegments.length >= 1 ? pathSegments[0] : this.doctype
287
+ const recordId = pathSegments.length >= 2 ? pathSegments[1] : undefined
288
+ const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1]
289
+
290
+ // Detect if this is a DELETE operation (setting to undefined when a value existed)
291
+ const isDelete = value === undefined && beforeValue !== undefined
292
+ const operationType: 'set' | 'delete' = isDelete ? 'delete' : 'set'
293
+
294
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
295
+ logStore.addOperation(
296
+ {
297
+ type: operationType,
298
+ path: fullPath,
299
+ fieldname,
300
+ beforeValue,
301
+ afterValue: value,
302
+ doctype,
303
+ recordId,
304
+ reversible: true, // Default to reversible, can be changed by field triggers
305
+ },
306
+ source
307
+ )
308
+ }
309
+ }
310
+
311
+ // Update the value
312
+ this.updateValue(path, value)
313
+
314
+ // Trigger field actions asynchronously (don't block the set operation)
315
+ void this.triggerFieldActions(fullPath, beforeValue, value)
316
+ }
317
+
318
+ has(path: string): boolean {
319
+ try {
320
+ // Handle empty path case
321
+ if (path === '') {
322
+ return true // empty path refers to the root object
323
+ }
324
+
325
+ const segments = this.parsePath(path)
326
+ let current = this.target
327
+
328
+ for (let i = 0; i < segments.length; i++) {
329
+ const segment = segments[i]
330
+
331
+ if (current === null || current === undefined) {
332
+ return false
333
+ }
334
+
335
+ // Check if this is the last segment
336
+ if (i === segments.length - 1) {
337
+ // For the final property, check if it exists
338
+ if (this.isImmutable(current)) {
339
+ return current.has(segment)
340
+ } else if (this.isPiniaStore(current)) {
341
+ return (current.$state && segment in current.$state) || segment in current
342
+ } else {
343
+ return segment in current
344
+ }
345
+ }
346
+
347
+ // Navigate to the next level
348
+ current = this.getProperty(current, segment)
349
+ }
350
+
351
+ return false
352
+ } catch {
353
+ return false
354
+ }
355
+ }
356
+
357
+ // Tree navigation methods
358
+ getParent(): HSTNode | null {
359
+ if (!this.parentPath) return null
360
+
361
+ const parentSegments = this.parentPath.split('.').slice(0, -1)
362
+ const parentPath = parentSegments.join('.')
363
+
364
+ if (parentPath === '') {
365
+ return this.rootNode
366
+ }
367
+
368
+ // Return a wrapped node, not raw data
369
+ return this.rootNode!.getNode(parentPath)
370
+ }
371
+
372
+ getRoot(): HSTNode {
373
+ return this.rootNode!
374
+ }
375
+
376
+ getPath(): string {
377
+ return this.parentPath
378
+ }
379
+
380
+ getDepth(): number {
381
+ return this.parentPath ? this.parentPath.split('.').length : 0
382
+ }
383
+
384
+ getBreadcrumbs(): string[] {
385
+ return this.parentPath ? this.parentPath.split('.') : []
386
+ }
387
+
388
+ /**
389
+ * Trigger an XState transition with optional context data
390
+ */
391
+ async triggerTransition(
392
+ transition: string,
393
+ context?: { currentState?: string; targetState?: string; fsmContext?: Record<string, any> }
394
+ ): Promise<any> {
395
+ const triggerEngine = getGlobalTriggerEngine()
396
+
397
+ // Determine doctype and recordId from the current path
398
+ const pathSegments = this.parentPath.split('.')
399
+ let doctype = this.doctype
400
+ let recordId: string | undefined
401
+
402
+ // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
403
+ if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
404
+ doctype = pathSegments[0]
405
+ }
406
+
407
+ // Extract recordId from path if it follows the expected pattern
408
+ if (pathSegments.length >= 2) {
409
+ recordId = pathSegments[1]
410
+ }
411
+
412
+ // Build transition context
413
+ const transitionContext: TransitionChangeContext = {
414
+ path: this.parentPath,
415
+ fieldname: '', // No specific field for transitions
416
+ beforeValue: undefined,
417
+ afterValue: undefined,
418
+ operation: 'set',
419
+ doctype,
420
+ recordId,
421
+ timestamp: new Date(),
422
+ store: this.rootNode || undefined,
423
+ transition,
424
+ currentState: context?.currentState,
425
+ targetState: context?.targetState,
426
+ fsmContext: context?.fsmContext,
427
+ }
428
+
429
+ // Log FSM transition operation
430
+ const logStore = getOperationLogStore()
431
+ if (logStore && typeof logStore.addOperation === 'function') {
432
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
433
+ logStore.addOperation(
434
+ {
435
+ type: 'transition' as const,
436
+ path: this.parentPath,
437
+ fieldname: transition,
438
+ beforeValue: context?.currentState,
439
+ afterValue: context?.targetState,
440
+ doctype,
441
+ recordId,
442
+ reversible: false, // FSM transitions are generally not reversible
443
+ metadata: {
444
+ transition,
445
+ currentState: context?.currentState,
446
+ targetState: context?.targetState,
447
+ fsmContext: context?.fsmContext,
448
+ },
449
+ },
450
+ 'user'
451
+ )
452
+ }
453
+
454
+ // Execute transition actions
455
+ return await triggerEngine.executeTransitionActions(transitionContext)
456
+ }
457
+
458
+ // Private helper methods
459
+ private resolvePath(path: string): string {
460
+ if (path === '') return this.parentPath
461
+ return this.parentPath ? `${this.parentPath}.${path}` : path
462
+ }
463
+
464
+ private resolveValue(path: string): any {
465
+ // Handle empty path - return the target object
466
+ if (path === '') {
467
+ return this.target
468
+ }
469
+
470
+ const segments = this.parsePath(path)
471
+ let current = this.target
472
+
473
+ for (const segment of segments) {
474
+ if (current === null || current === undefined) {
475
+ return undefined
476
+ }
477
+
478
+ current = this.getProperty(current, segment)
479
+ }
480
+
481
+ return current
482
+ }
483
+
484
+ private updateValue(path: string, value: any): void {
485
+ // Handle empty path case - should throw error
486
+ if (path === '') {
487
+ throw new Error('Cannot set value on empty path')
488
+ }
489
+
490
+ const segments = this.parsePath(path)
491
+ const lastSegment = segments.pop()!
492
+ let current = this.target
493
+
494
+ // Navigate to parent object
495
+ for (const segment of segments) {
496
+ current = this.getProperty(current, segment)
497
+ if (current === null || current === undefined) {
498
+ throw new Error(`Cannot set property on null/undefined path: ${path}`)
499
+ }
500
+ }
501
+
502
+ // Set the final property
503
+ this.setProperty(current, lastSegment, value)
504
+ }
505
+
506
+ private getProperty(obj: any, key: string): any {
507
+ // Immutable objects
508
+ if (this.isImmutable(obj)) {
509
+ return obj.get(key)
510
+ }
511
+
512
+ // Vue reactive object
513
+ if (this.isVueReactive(obj)) {
514
+ return obj[key]
515
+ }
516
+
517
+ // Pinia store
518
+ if (this.isPiniaStore(obj)) {
519
+ return obj.$state?.[key] ?? obj[key]
520
+ }
521
+
522
+ // Plain object
523
+ return (obj as PropertyAccessible)[key]
524
+ }
525
+
526
+ private setProperty(obj: any, key: string, value: any): void {
527
+ // Immutable objects
528
+ if (this.isImmutable(obj)) {
529
+ throw new Error('Cannot directly mutate immutable objects. Use immutable update methods instead.')
530
+ }
531
+
532
+ // Pinia store
533
+ if (this.isPiniaStore(obj)) {
534
+ if (obj.$patch) {
535
+ obj.$patch({ [key]: value })
536
+ } else {
537
+ ;(obj as PropertyAccessible)[key] = value
538
+ }
539
+ return
540
+ }
541
+
542
+ // Vue reactive or plain object
543
+ ;(obj as PropertyAccessible)[key] = value
544
+ }
545
+
546
+ private async triggerFieldActions(fullPath: string, beforeValue: any, afterValue: any): Promise<void> {
547
+ try {
548
+ // Guard against undefined or null fullPath
549
+ if (!fullPath || typeof fullPath !== 'string') {
550
+ return
551
+ }
552
+
553
+ const pathSegments = fullPath.split('.')
554
+
555
+ // Only trigger field actions for actual field changes (at least 3 levels deep: doctype.recordId.fieldname)
556
+ // Skip triggering for doctype-level or record-level changes
557
+ if (pathSegments.length < 3) {
558
+ return
559
+ }
560
+
561
+ const triggerEngine = getGlobalTriggerEngine()
562
+ const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1]
563
+
564
+ // Determine the correct doctype for this path using the same logic as getNode()
565
+ // The path should be in format: "doctype.recordId.fieldname"
566
+ let doctype = this.doctype
567
+
568
+ // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
569
+ if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
570
+ doctype = pathSegments[0]
571
+ }
572
+
573
+ let recordId: string | undefined
574
+
575
+ // Extract recordId from path if it follows the expected pattern
576
+ if (pathSegments.length >= 2) {
577
+ recordId = pathSegments[1]
578
+ }
579
+
580
+ const context: FieldChangeContext = {
581
+ path: fullPath,
582
+ fieldname,
583
+ beforeValue,
584
+ afterValue,
585
+ operation: 'set',
586
+ doctype,
587
+ recordId,
588
+ timestamp: new Date(),
589
+ store: this.rootNode || undefined, // Pass the root store for snapshot/rollback capabilities
590
+ }
591
+
592
+ await triggerEngine.executeFieldTriggers(context)
593
+ } catch (error) {
594
+ // Silently handle trigger errors to not break the main flow
595
+ // In production, you might want to log this error
596
+ if (error instanceof Error) {
597
+ // eslint-disable-next-line no-console
598
+ console.warn('Field trigger error:', error.message)
599
+ // Optional: emit an event or call error handler
600
+ }
601
+ }
602
+ }
603
+ private isVueReactive(obj: any): obj is VueReactive {
604
+ return (
605
+ obj &&
606
+ typeof obj === 'object' &&
607
+ '__v_isReactive' in obj &&
608
+ (obj as { __v_isReactive: boolean }).__v_isReactive === true
609
+ )
610
+ }
611
+
612
+ private isPiniaStore(obj: any): obj is PiniaStore {
613
+ return obj && typeof obj === 'object' && ('$state' in obj || '$patch' in obj || '$id' in obj)
614
+ }
615
+
616
+ private isImmutable(obj: any): obj is ImmutableLike {
617
+ if (!obj || typeof obj !== 'object') {
618
+ return false
619
+ }
620
+
621
+ const hasGetMethod = 'get' in obj && typeof (obj as Record<string, unknown>).get === 'function'
622
+ const hasSetMethod = 'set' in obj && typeof (obj as Record<string, unknown>).set === 'function'
623
+ const hasHasMethod = 'has' in obj && typeof (obj as Record<string, unknown>).has === 'function'
624
+
625
+ const hasImmutableMarkers =
626
+ '__ownerID' in obj ||
627
+ '_map' in obj ||
628
+ '_list' in obj ||
629
+ '_origin' in obj ||
630
+ '_capacity' in obj ||
631
+ '_defaultValues' in obj ||
632
+ '_tail' in obj ||
633
+ '_root' in obj ||
634
+ ('size' in obj && hasGetMethod && hasSetMethod)
635
+
636
+ let constructorName: string | undefined
637
+ try {
638
+ const objWithConstructor = obj as Record<string, unknown>
639
+ if (
640
+ 'constructor' in objWithConstructor &&
641
+ objWithConstructor.constructor &&
642
+ typeof objWithConstructor.constructor === 'object' &&
643
+ 'name' in objWithConstructor.constructor
644
+ ) {
645
+ const nameValue = (objWithConstructor.constructor as { name: unknown }).name
646
+ constructorName = typeof nameValue === 'string' ? nameValue : undefined
647
+ }
648
+ } catch {
649
+ constructorName = undefined
650
+ }
651
+
652
+ const isImmutableConstructor =
653
+ constructorName &&
654
+ (constructorName.includes('Map') ||
655
+ constructorName.includes('List') ||
656
+ constructorName.includes('Set') ||
657
+ constructorName.includes('Stack') ||
658
+ constructorName.includes('Seq')) &&
659
+ (hasGetMethod || hasSetMethod)
660
+
661
+ return Boolean(
662
+ (hasGetMethod && hasSetMethod && hasHasMethod && hasImmutableMarkers) ||
663
+ (hasGetMethod && hasSetMethod && isImmutableConstructor)
664
+ )
665
+ }
666
+
667
+ private isPrimitive(value: any): boolean {
668
+ // Don't wrap primitive values, functions, or null/undefined
669
+ return (
670
+ value === null ||
671
+ value === undefined ||
672
+ typeof value === 'string' ||
673
+ typeof value === 'number' ||
674
+ typeof value === 'boolean' ||
675
+ typeof value === 'function' ||
676
+ typeof value === 'symbol' ||
677
+ typeof value === 'bigint'
678
+ )
679
+ }
680
+
681
+ private parsePath(path: string): string[] {
682
+ if (!path) return []
683
+ return path.split('.').filter(segment => segment.length > 0)
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Factory function for HST creation
689
+ * Creates a new HSTNode proxy for hierarchical state tree navigation.
690
+ *
691
+ * @param target - The target object to wrap with HST functionality
692
+ * @param doctype - The document type identifier
693
+ * @param parentDoctype - Optional parent document type identifier
694
+ * @returns A new HSTNode proxy instance
695
+ *
696
+ * @public
697
+ */
698
+ function createHST(target: any, doctype: string, parentDoctype?: string): HSTNode {
699
+ return new HSTProxy(target, doctype, '', null, parentDoctype)
700
+ }
701
+
702
+ // Export everything
703
+ export { HSTProxy, HST, createHST, type HSTNode }
@@ -1,6 +1,9 @@
1
1
  import { createPinia } from 'pinia'
2
2
  import { PiniaSharedState } from 'pinia-shared-state'
3
3
 
4
+ import { HST } from './hst'
5
+
6
+ const hst = HST.getInstance()
4
7
  const pinia = createPinia()
5
8
 
6
9
  // Pass the plugin to your application's pinia plugin
@@ -11,4 +14,6 @@ pinia.use(
11
14
  })
12
15
  )
13
16
 
14
- export { pinia }
17
+ export { hst, pinia }
18
+ export { useOperationLogStore } from './operation-log'
19
+ export type { HSTNode } from './hst'