@stonecrop/stonecrop 0.6.3 → 0.7.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.
@@ -1,483 +0,0 @@
1
- import { getGlobalTriggerEngine } from '../field-triggers';
2
- import { useOperationLogStore } from './operation-log';
3
- /**
4
- * Get the operation log store if available
5
- */
6
- function getOperationLogStore() {
7
- try {
8
- return useOperationLogStore();
9
- }
10
- catch {
11
- // Operation log is optional
12
- return null;
13
- }
14
- }
15
- /**
16
- * Global HST Manager (Singleton)
17
- * Manages hierarchical state trees and provides access to the global registry.
18
- *
19
- * @public
20
- */
21
- class HST {
22
- static instance;
23
- /**
24
- * Gets the singleton instance of HST
25
- * @returns The HST singleton instance
26
- */
27
- static getInstance() {
28
- if (!HST.instance) {
29
- HST.instance = new HST();
30
- }
31
- return HST.instance;
32
- }
33
- /**
34
- * Gets the global registry instance
35
- * @returns The global registry object or undefined if not found
36
- */
37
- getRegistry() {
38
- // In test environment, try different ways to access Registry
39
- // First, try the global Registry if it exists
40
- if (typeof globalThis !== 'undefined') {
41
- const globalRegistry = globalThis.Registry?._root;
42
- if (globalRegistry) {
43
- return globalRegistry;
44
- }
45
- }
46
- // Try to access through window (browser environment)
47
- if (typeof window !== 'undefined') {
48
- const windowRegistry = window.Registry?._root;
49
- if (windowRegistry) {
50
- return windowRegistry;
51
- }
52
- }
53
- // Try to access through global (Node environment)
54
- if (typeof global !== 'undefined' && global) {
55
- const nodeRegistry = global.Registry?._root;
56
- if (nodeRegistry) {
57
- return nodeRegistry;
58
- }
59
- }
60
- // If we can't find it globally, it might not be set up
61
- // This is expected in test environments where Registry is created locally
62
- return undefined;
63
- }
64
- /**
65
- * Helper method to get doctype metadata from the registry
66
- * @param doctype - The name of the doctype to retrieve metadata for
67
- * @returns The doctype metadata object or undefined if not found
68
- */
69
- getDoctypeMeta(doctype) {
70
- const registry = this.getRegistry();
71
- if (registry && typeof registry === 'object' && 'registry' in registry) {
72
- return registry.registry[doctype];
73
- }
74
- return undefined;
75
- }
76
- }
77
- // Enhanced HST Proxy with tree navigation
78
- class HSTProxy {
79
- target;
80
- parentPath;
81
- rootNode;
82
- doctype;
83
- parentDoctype;
84
- hst;
85
- constructor(target, doctype, parentPath = '', rootNode = null, parentDoctype) {
86
- this.target = target;
87
- this.parentPath = parentPath;
88
- this.rootNode = rootNode || this;
89
- this.doctype = doctype;
90
- this.parentDoctype = parentDoctype;
91
- this.hst = HST.getInstance();
92
- return new Proxy(this, {
93
- get(hst, prop) {
94
- // Return HST methods directly
95
- if (prop in hst)
96
- return hst[prop];
97
- // Handle property access - return tree nodes for navigation
98
- const path = String(prop);
99
- return hst.getNode(path);
100
- },
101
- set(hst, prop, value) {
102
- const path = String(prop);
103
- hst.set(path, value);
104
- return true;
105
- },
106
- });
107
- }
108
- get(path) {
109
- return this.resolveValue(path);
110
- }
111
- // Method to get a tree-wrapped node for navigation
112
- getNode(path) {
113
- const fullPath = this.resolvePath(path);
114
- const value = this.resolveValue(path);
115
- // Determine the correct doctype for this node based on the path
116
- const pathSegments = fullPath.split('.');
117
- let nodeDoctype = this.doctype;
118
- // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
119
- if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
120
- nodeDoctype = pathSegments[0];
121
- }
122
- // Always wrap in HSTProxy for tree navigation
123
- if (typeof value === 'object' && value !== null && !this.isPrimitive(value)) {
124
- return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode, this.parentDoctype);
125
- }
126
- // For primitives, return a minimal wrapper that throws on tree operations
127
- return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode, this.parentDoctype);
128
- }
129
- set(path, value, source = 'user') {
130
- // Get current value for change context
131
- const fullPath = this.resolvePath(path);
132
- const beforeValue = this.has(path) ? this.get(path) : undefined;
133
- // Log operation if not from undo/redo and store is available
134
- if (source !== 'undo' && source !== 'redo') {
135
- const logStore = getOperationLogStore();
136
- if (logStore && typeof logStore.addOperation === 'function') {
137
- const pathSegments = fullPath.split('.');
138
- const doctype = this.doctype === 'StonecropStore' && pathSegments.length >= 1 ? pathSegments[0] : this.doctype;
139
- const recordId = pathSegments.length >= 2 ? pathSegments[1] : undefined;
140
- const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1];
141
- // Detect if this is a DELETE operation (setting to undefined when a value existed)
142
- const isDelete = value === undefined && beforeValue !== undefined;
143
- const operationType = isDelete ? 'delete' : 'set';
144
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
145
- logStore.addOperation({
146
- type: operationType,
147
- path: fullPath,
148
- fieldname,
149
- beforeValue,
150
- afterValue: value,
151
- doctype,
152
- recordId,
153
- reversible: true, // Default to reversible, can be changed by field triggers
154
- }, source);
155
- }
156
- }
157
- // Update the value
158
- this.updateValue(path, value);
159
- // Trigger field actions asynchronously (don't block the set operation)
160
- void this.triggerFieldActions(fullPath, beforeValue, value);
161
- }
162
- has(path) {
163
- try {
164
- // Handle empty path case
165
- if (path === '') {
166
- return true; // empty path refers to the root object
167
- }
168
- const segments = this.parsePath(path);
169
- let current = this.target;
170
- for (let i = 0; i < segments.length; i++) {
171
- const segment = segments[i];
172
- if (current === null || current === undefined) {
173
- return false;
174
- }
175
- // Check if this is the last segment
176
- if (i === segments.length - 1) {
177
- // For the final property, check if it exists
178
- if (this.isImmutable(current)) {
179
- return current.has(segment);
180
- }
181
- else if (this.isPiniaStore(current)) {
182
- return (current.$state && segment in current.$state) || segment in current;
183
- }
184
- else {
185
- return segment in current;
186
- }
187
- }
188
- // Navigate to the next level
189
- current = this.getProperty(current, segment);
190
- }
191
- return false;
192
- }
193
- catch {
194
- return false;
195
- }
196
- }
197
- // Tree navigation methods
198
- getParent() {
199
- if (!this.parentPath)
200
- return null;
201
- const parentSegments = this.parentPath.split('.').slice(0, -1);
202
- const parentPath = parentSegments.join('.');
203
- if (parentPath === '') {
204
- return this.rootNode;
205
- }
206
- // Return a wrapped node, not raw data
207
- return this.rootNode.getNode(parentPath);
208
- }
209
- getRoot() {
210
- return this.rootNode;
211
- }
212
- getPath() {
213
- return this.parentPath;
214
- }
215
- getDepth() {
216
- return this.parentPath ? this.parentPath.split('.').length : 0;
217
- }
218
- getBreadcrumbs() {
219
- return this.parentPath ? this.parentPath.split('.') : [];
220
- }
221
- /**
222
- * Trigger an XState transition with optional context data
223
- */
224
- async triggerTransition(transition, context) {
225
- const triggerEngine = getGlobalTriggerEngine();
226
- // Determine doctype and recordId from the current path
227
- const pathSegments = this.parentPath.split('.');
228
- let doctype = this.doctype;
229
- let recordId;
230
- // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
231
- if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
232
- doctype = pathSegments[0];
233
- }
234
- // Extract recordId from path if it follows the expected pattern
235
- if (pathSegments.length >= 2) {
236
- recordId = pathSegments[1];
237
- }
238
- // Build transition context
239
- const transitionContext = {
240
- path: this.parentPath,
241
- fieldname: '', // No specific field for transitions
242
- beforeValue: undefined,
243
- afterValue: undefined,
244
- operation: 'set',
245
- doctype,
246
- recordId,
247
- timestamp: new Date(),
248
- store: this.rootNode || undefined,
249
- transition,
250
- currentState: context?.currentState,
251
- targetState: context?.targetState,
252
- fsmContext: context?.fsmContext,
253
- };
254
- // Log FSM transition operation
255
- const logStore = getOperationLogStore();
256
- if (logStore && typeof logStore.addOperation === 'function') {
257
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
258
- logStore.addOperation({
259
- type: 'transition',
260
- path: this.parentPath,
261
- fieldname: transition,
262
- beforeValue: context?.currentState,
263
- afterValue: context?.targetState,
264
- doctype,
265
- recordId,
266
- reversible: false, // FSM transitions are generally not reversible
267
- metadata: {
268
- transition,
269
- currentState: context?.currentState,
270
- targetState: context?.targetState,
271
- fsmContext: context?.fsmContext,
272
- },
273
- }, 'user');
274
- }
275
- // Execute transition actions
276
- return await triggerEngine.executeTransitionActions(transitionContext);
277
- }
278
- // Private helper methods
279
- resolvePath(path) {
280
- if (path === '')
281
- return this.parentPath;
282
- return this.parentPath ? `${this.parentPath}.${path}` : path;
283
- }
284
- resolveValue(path) {
285
- // Handle empty path - return the target object
286
- if (path === '') {
287
- return this.target;
288
- }
289
- const segments = this.parsePath(path);
290
- let current = this.target;
291
- for (const segment of segments) {
292
- if (current === null || current === undefined) {
293
- return undefined;
294
- }
295
- current = this.getProperty(current, segment);
296
- }
297
- return current;
298
- }
299
- updateValue(path, value) {
300
- // Handle empty path case - should throw error
301
- if (path === '') {
302
- throw new Error('Cannot set value on empty path');
303
- }
304
- const segments = this.parsePath(path);
305
- const lastSegment = segments.pop();
306
- let current = this.target;
307
- // Navigate to parent object
308
- for (const segment of segments) {
309
- current = this.getProperty(current, segment);
310
- if (current === null || current === undefined) {
311
- throw new Error(`Cannot set property on null/undefined path: ${path}`);
312
- }
313
- }
314
- // Set the final property
315
- this.setProperty(current, lastSegment, value);
316
- }
317
- getProperty(obj, key) {
318
- // Immutable objects
319
- if (this.isImmutable(obj)) {
320
- return obj.get(key);
321
- }
322
- // Vue reactive object
323
- if (this.isVueReactive(obj)) {
324
- return obj[key];
325
- }
326
- // Pinia store
327
- if (this.isPiniaStore(obj)) {
328
- return obj.$state?.[key] ?? obj[key];
329
- }
330
- // Plain object
331
- return obj[key];
332
- }
333
- setProperty(obj, key, value) {
334
- // Immutable objects
335
- if (this.isImmutable(obj)) {
336
- throw new Error('Cannot directly mutate immutable objects. Use immutable update methods instead.');
337
- }
338
- // Pinia store
339
- if (this.isPiniaStore(obj)) {
340
- if (obj.$patch) {
341
- obj.$patch({ [key]: value });
342
- }
343
- else {
344
- ;
345
- obj[key] = value;
346
- }
347
- return;
348
- }
349
- // Vue reactive or plain object
350
- ;
351
- obj[key] = value;
352
- }
353
- async triggerFieldActions(fullPath, beforeValue, afterValue) {
354
- try {
355
- // Guard against undefined or null fullPath
356
- if (!fullPath || typeof fullPath !== 'string') {
357
- return;
358
- }
359
- const pathSegments = fullPath.split('.');
360
- // Only trigger field actions for actual field changes (at least 3 levels deep: doctype.recordId.fieldname)
361
- // Skip triggering for doctype-level or record-level changes
362
- if (pathSegments.length < 3) {
363
- return;
364
- }
365
- const triggerEngine = getGlobalTriggerEngine();
366
- const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1];
367
- // Determine the correct doctype for this path using the same logic as getNode()
368
- // The path should be in format: "doctype.recordId.fieldname"
369
- let doctype = this.doctype;
370
- // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
371
- if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
372
- doctype = pathSegments[0];
373
- }
374
- let recordId;
375
- // Extract recordId from path if it follows the expected pattern
376
- if (pathSegments.length >= 2) {
377
- recordId = pathSegments[1];
378
- }
379
- const context = {
380
- path: fullPath,
381
- fieldname,
382
- beforeValue,
383
- afterValue,
384
- operation: 'set',
385
- doctype,
386
- recordId,
387
- timestamp: new Date(),
388
- store: this.rootNode || undefined, // Pass the root store for snapshot/rollback capabilities
389
- };
390
- await triggerEngine.executeFieldTriggers(context);
391
- }
392
- catch (error) {
393
- // Silently handle trigger errors to not break the main flow
394
- // In production, you might want to log this error
395
- if (error instanceof Error) {
396
- // eslint-disable-next-line no-console
397
- console.warn('Field trigger error:', error.message);
398
- // Optional: emit an event or call error handler
399
- }
400
- }
401
- }
402
- isVueReactive(obj) {
403
- return (obj &&
404
- typeof obj === 'object' &&
405
- '__v_isReactive' in obj &&
406
- obj.__v_isReactive === true);
407
- }
408
- isPiniaStore(obj) {
409
- return obj && typeof obj === 'object' && ('$state' in obj || '$patch' in obj || '$id' in obj);
410
- }
411
- isImmutable(obj) {
412
- if (!obj || typeof obj !== 'object') {
413
- return false;
414
- }
415
- const hasGetMethod = 'get' in obj && typeof obj.get === 'function';
416
- const hasSetMethod = 'set' in obj && typeof obj.set === 'function';
417
- const hasHasMethod = 'has' in obj && typeof obj.has === 'function';
418
- const hasImmutableMarkers = '__ownerID' in obj ||
419
- '_map' in obj ||
420
- '_list' in obj ||
421
- '_origin' in obj ||
422
- '_capacity' in obj ||
423
- '_defaultValues' in obj ||
424
- '_tail' in obj ||
425
- '_root' in obj ||
426
- ('size' in obj && hasGetMethod && hasSetMethod);
427
- let constructorName;
428
- try {
429
- const objWithConstructor = obj;
430
- if ('constructor' in objWithConstructor &&
431
- objWithConstructor.constructor &&
432
- typeof objWithConstructor.constructor === 'object' &&
433
- 'name' in objWithConstructor.constructor) {
434
- const nameValue = objWithConstructor.constructor.name;
435
- constructorName = typeof nameValue === 'string' ? nameValue : undefined;
436
- }
437
- }
438
- catch {
439
- constructorName = undefined;
440
- }
441
- const isImmutableConstructor = constructorName &&
442
- (constructorName.includes('Map') ||
443
- constructorName.includes('List') ||
444
- constructorName.includes('Set') ||
445
- constructorName.includes('Stack') ||
446
- constructorName.includes('Seq')) &&
447
- (hasGetMethod || hasSetMethod);
448
- return Boolean((hasGetMethod && hasSetMethod && hasHasMethod && hasImmutableMarkers) ||
449
- (hasGetMethod && hasSetMethod && isImmutableConstructor));
450
- }
451
- isPrimitive(value) {
452
- // Don't wrap primitive values, functions, or null/undefined
453
- return (value === null ||
454
- value === undefined ||
455
- typeof value === 'string' ||
456
- typeof value === 'number' ||
457
- typeof value === 'boolean' ||
458
- typeof value === 'function' ||
459
- typeof value === 'symbol' ||
460
- typeof value === 'bigint');
461
- }
462
- parsePath(path) {
463
- if (!path)
464
- return [];
465
- return path.split('.').filter(segment => segment.length > 0);
466
- }
467
- }
468
- /**
469
- * Factory function for HST creation
470
- * Creates a new HSTNode proxy for hierarchical state tree navigation.
471
- *
472
- * @param target - The target object to wrap with HST functionality
473
- * @param doctype - The document type identifier
474
- * @param parentDoctype - Optional parent document type identifier
475
- * @returns A new HSTNode proxy instance
476
- *
477
- * @public
478
- */
479
- function createHST(target, doctype, parentDoctype) {
480
- return new HSTProxy(target, doctype, '', null, parentDoctype);
481
- }
482
- // Export everything
483
- export { HSTProxy, HST, createHST };
@@ -1,12 +0,0 @@
1
- import { createPinia } from 'pinia';
2
- import { PiniaSharedState } from 'pinia-shared-state';
3
- import { HST } from './hst';
4
- const hst = HST.getInstance();
5
- const pinia = createPinia();
6
- // Pass the plugin to your application's pinia plugin
7
- pinia.use(PiniaSharedState({
8
- enable: true,
9
- initialize: true,
10
- }));
11
- export { hst, pinia };
12
- export { useOperationLogStore } from './operation-log';