@creact-labs/creact 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.
Files changed (103) hide show
  1. package/LICENSE +212 -0
  2. package/README.md +379 -0
  3. package/dist/cli/commands/BuildCommand.d.ts +40 -0
  4. package/dist/cli/commands/BuildCommand.js +151 -0
  5. package/dist/cli/commands/DeployCommand.d.ts +38 -0
  6. package/dist/cli/commands/DeployCommand.js +194 -0
  7. package/dist/cli/commands/DevCommand.d.ts +52 -0
  8. package/dist/cli/commands/DevCommand.js +385 -0
  9. package/dist/cli/commands/PlanCommand.d.ts +39 -0
  10. package/dist/cli/commands/PlanCommand.js +164 -0
  11. package/dist/cli/commands/index.d.ts +36 -0
  12. package/dist/cli/commands/index.js +43 -0
  13. package/dist/cli/core/ArgumentParser.d.ts +46 -0
  14. package/dist/cli/core/ArgumentParser.js +127 -0
  15. package/dist/cli/core/BaseCommand.d.ts +75 -0
  16. package/dist/cli/core/BaseCommand.js +95 -0
  17. package/dist/cli/core/CLIContext.d.ts +68 -0
  18. package/dist/cli/core/CLIContext.js +183 -0
  19. package/dist/cli/core/CommandRegistry.d.ts +64 -0
  20. package/dist/cli/core/CommandRegistry.js +89 -0
  21. package/dist/cli/core/index.d.ts +36 -0
  22. package/dist/cli/core/index.js +43 -0
  23. package/dist/cli/index.d.ts +35 -0
  24. package/dist/cli/index.js +100 -0
  25. package/dist/cli/output.d.ts +204 -0
  26. package/dist/cli/output.js +437 -0
  27. package/dist/cli/utils.d.ts +59 -0
  28. package/dist/cli/utils.js +76 -0
  29. package/dist/context/createContext.d.ts +90 -0
  30. package/dist/context/createContext.js +113 -0
  31. package/dist/context/index.d.ts +30 -0
  32. package/dist/context/index.js +35 -0
  33. package/dist/core/CReact.d.ts +409 -0
  34. package/dist/core/CReact.js +1127 -0
  35. package/dist/core/CloudDOMBuilder.d.ts +429 -0
  36. package/dist/core/CloudDOMBuilder.js +1198 -0
  37. package/dist/core/ContextDependencyTracker.d.ts +165 -0
  38. package/dist/core/ContextDependencyTracker.js +448 -0
  39. package/dist/core/ErrorRecoveryManager.d.ts +145 -0
  40. package/dist/core/ErrorRecoveryManager.js +443 -0
  41. package/dist/core/EventBus.d.ts +91 -0
  42. package/dist/core/EventBus.js +185 -0
  43. package/dist/core/ProviderOutputTracker.d.ts +211 -0
  44. package/dist/core/ProviderOutputTracker.js +476 -0
  45. package/dist/core/ReactiveUpdateQueue.d.ts +76 -0
  46. package/dist/core/ReactiveUpdateQueue.js +121 -0
  47. package/dist/core/Reconciler.d.ts +415 -0
  48. package/dist/core/Reconciler.js +1037 -0
  49. package/dist/core/RenderScheduler.d.ts +153 -0
  50. package/dist/core/RenderScheduler.js +519 -0
  51. package/dist/core/Renderer.d.ts +276 -0
  52. package/dist/core/Renderer.js +791 -0
  53. package/dist/core/Runtime.d.ts +246 -0
  54. package/dist/core/Runtime.js +640 -0
  55. package/dist/core/StateBindingManager.d.ts +121 -0
  56. package/dist/core/StateBindingManager.js +309 -0
  57. package/dist/core/StateMachine.d.ts +424 -0
  58. package/dist/core/StateMachine.js +787 -0
  59. package/dist/core/StructuralChangeDetector.d.ts +140 -0
  60. package/dist/core/StructuralChangeDetector.js +363 -0
  61. package/dist/core/Validator.d.ts +127 -0
  62. package/dist/core/Validator.js +279 -0
  63. package/dist/core/errors.d.ts +153 -0
  64. package/dist/core/errors.js +202 -0
  65. package/dist/core/index.d.ts +38 -0
  66. package/dist/core/index.js +64 -0
  67. package/dist/core/types.d.ts +263 -0
  68. package/dist/core/types.js +48 -0
  69. package/dist/hooks/context.d.ts +147 -0
  70. package/dist/hooks/context.js +334 -0
  71. package/dist/hooks/useContext.d.ts +113 -0
  72. package/dist/hooks/useContext.js +169 -0
  73. package/dist/hooks/useEffect.d.ts +105 -0
  74. package/dist/hooks/useEffect.js +540 -0
  75. package/dist/hooks/useInstance.d.ts +139 -0
  76. package/dist/hooks/useInstance.js +441 -0
  77. package/dist/hooks/useState.d.ts +120 -0
  78. package/dist/hooks/useState.js +298 -0
  79. package/dist/index.d.ts +46 -0
  80. package/dist/index.js +70 -0
  81. package/dist/jsx.d.ts +64 -0
  82. package/dist/jsx.js +76 -0
  83. package/dist/providers/DummyBackendProvider.d.ts +193 -0
  84. package/dist/providers/DummyBackendProvider.js +189 -0
  85. package/dist/providers/DummyCloudProvider.d.ts +128 -0
  86. package/dist/providers/DummyCloudProvider.js +157 -0
  87. package/dist/providers/IBackendProvider.d.ts +177 -0
  88. package/dist/providers/IBackendProvider.js +31 -0
  89. package/dist/providers/ICloudProvider.d.ts +146 -0
  90. package/dist/providers/ICloudProvider.js +31 -0
  91. package/dist/providers/index.d.ts +31 -0
  92. package/dist/providers/index.js +31 -0
  93. package/dist/test-event-callbacks.d.ts +0 -0
  94. package/dist/test-event-callbacks.js +1 -0
  95. package/dist/utils/Logger.d.ts +144 -0
  96. package/dist/utils/Logger.js +220 -0
  97. package/dist/utils/Output.d.ts +161 -0
  98. package/dist/utils/Output.js +401 -0
  99. package/dist/utils/deepEqual.d.ts +71 -0
  100. package/dist/utils/deepEqual.js +276 -0
  101. package/dist/utils/naming.d.ts +241 -0
  102. package/dist/utils/naming.js +376 -0
  103. package/package.json +87 -0
@@ -0,0 +1,1198 @@
1
+ "use strict";
2
+ /**
3
+
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+
6
+ * you may not use this file except in compliance with the License.
7
+
8
+ * You may obtain a copy of the License at
9
+
10
+ *
11
+
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+
14
+ *
15
+
16
+ * Unless required by applicable law or agreed to in writing, software
17
+
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+
20
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+
22
+ * See the License for the specific language governing permissions and
23
+
24
+ * limitations under the License.
25
+
26
+ *
27
+
28
+ * Copyright 2025 Daniel Coutinho Ribeiro
29
+
30
+ */
31
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ var desc = Object.getOwnPropertyDescriptor(m, k);
34
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
35
+ desc = { enumerable: true, get: function() { return m[k]; } };
36
+ }
37
+ Object.defineProperty(o, k2, desc);
38
+ }) : (function(o, m, k, k2) {
39
+ if (k2 === undefined) k2 = k;
40
+ o[k2] = m[k];
41
+ }));
42
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
43
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
44
+ }) : function(o, v) {
45
+ o["default"] = v;
46
+ });
47
+ var __importStar = (this && this.__importStar) || (function () {
48
+ var ownKeys = function(o) {
49
+ ownKeys = Object.getOwnPropertyNames || function (o) {
50
+ var ar = [];
51
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
52
+ return ar;
53
+ };
54
+ return ownKeys(o);
55
+ };
56
+ return function (mod) {
57
+ if (mod && mod.__esModule) return mod;
58
+ var result = {};
59
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
60
+ __setModuleDefault(result, mod);
61
+ return result;
62
+ };
63
+ })();
64
+ Object.defineProperty(exports, "__esModule", { value: true });
65
+ exports.CloudDOMBuilder = void 0;
66
+ const naming_1 = require("../utils/naming");
67
+ const ReactiveUpdateQueue_1 = require("./ReactiveUpdateQueue");
68
+ const Logger_1 = require("../utils/Logger");
69
+ const logger = Logger_1.LoggerFactory.getLogger('clouddom');
70
+ /**
71
+ * CloudDOMBuilder transforms a Fiber tree into a CloudDOM tree
72
+ *
73
+ * The CloudDOM tree represents actual cloud resources to be deployed:
74
+ * - Only includes components that called useInstance (have cloudDOMNode attached)
75
+ * - Filters out container components (no useInstance)
76
+ * - Builds parent-child relationships for deployment order
77
+ * - Generates resource IDs from hierarchical paths
78
+ *
79
+ * REQ-01: JSX → CloudDOM rendering
80
+ * REQ-04: Receives ICloudProvider via dependency injection (not used in build, but available for future extensions)
81
+ */
82
+ class CloudDOMBuilder {
83
+ /**
84
+ * Constructor receives ICloudProvider via dependency injection
85
+ *
86
+ * REQ-04: Dependency injection pattern - provider is injected, not inherited
87
+ *
88
+ * @param cloudProvider - Cloud provider implementation (injected)
89
+ */
90
+ constructor(cloudProvider) {
91
+ this.cloudProvider = cloudProvider;
92
+ /**
93
+ * Track existing nodes to handle re-render scenarios
94
+ * Maps node ID to fiber path for duplicate detection during re-renders
95
+ */
96
+ this.existingNodeMap = new Map();
97
+ // Provider is stored for potential future use (e.g., provider-specific CloudDOM transformations)
98
+ // For POC, we don't use it during build, but it's available for extensions
99
+ }
100
+ /**
101
+ * Set lifecycle hooks for integration with other components
102
+ *
103
+ * Supports async hooks for:
104
+ * - beforeBuild: Validation, pre-processing, telemetry
105
+ * - afterBuild: Provider preparation, post-processing, logging
106
+ *
107
+ * Example:
108
+ * ```typescript
109
+ * builder.setHooks({
110
+ * beforeBuild: async fiber => validator.validate(fiber),
111
+ * afterBuild: async tree => cloudProvider.prepare(tree),
112
+ * });
113
+ * ```
114
+ *
115
+ * @param hooks - Optional lifecycle hooks (sync or async)
116
+ */
117
+ setHooks(hooks) {
118
+ this.beforeBuildHook = hooks.beforeBuild;
119
+ this.afterBuildHook = hooks.afterBuild;
120
+ }
121
+ /**
122
+ * Set reactive components for output synchronization
123
+ *
124
+ * @param stateBindingManager - State binding manager instance
125
+ * @param providerOutputTracker - Provider output tracker instance
126
+ */
127
+ setReactiveComponents(stateBindingManager, providerOutputTracker) {
128
+ this.stateBindingManager = stateBindingManager;
129
+ this.providerOutputTracker = providerOutputTracker;
130
+ }
131
+ /**
132
+ * Build CloudDOM tree from Fiber tree
133
+ *
134
+ * Traverses the Fiber tree and collects nodes that have cloudDOMNode attached
135
+ * (i.e., components that called useInstance). Container components without
136
+ * useInstance are filtered out, but their children are preserved.
137
+ *
138
+ * Supports async lifecycle hooks for validation and provider preparation.
139
+ *
140
+ * REQ-01: Transform Fiber → CloudDOM
141
+ *
142
+ * @param fiber - Root Fiber node
143
+ * @returns Promise resolving to array of CloudDOM nodes (top-level resources)
144
+ */
145
+ async build(fiber) {
146
+ if (!fiber) {
147
+ throw new Error('[CloudDOMBuilder] Cannot build CloudDOM from null Fiber tree');
148
+ }
149
+ // Lifecycle hook: beforeBuild (supports async, isolated errors)
150
+ if (this.beforeBuildHook) {
151
+ try {
152
+ await this.beforeBuildHook(fiber);
153
+ }
154
+ catch (err) {
155
+ // Re-throw validation errors (expected to halt build)
156
+ // But log and continue for non-critical hooks (telemetry, logging)
157
+ if (err instanceof Error && err.message.includes('Validation')) {
158
+ throw err;
159
+ }
160
+ logger.error('beforeBuild hook failed (non-critical):', err);
161
+ }
162
+ }
163
+ // Collect all CloudDOM nodes from the Fiber tree
164
+ const cloudDOMNodes = [];
165
+ this.collectCloudDOMNodes(fiber, cloudDOMNodes);
166
+ // Validate collected nodes
167
+ this.validateCloudDOMNodes(cloudDOMNodes);
168
+ // Build parent-child relationships
169
+ // CRITICAL: Work with original nodes, not copies, so enhanced proxies see updates
170
+ const rootNodes = this.buildHierarchy(cloudDOMNodes);
171
+ // Detect circular references in hierarchy
172
+ this.detectCircularRefs(rootNodes);
173
+ // Sort root nodes for deterministic order
174
+ rootNodes.sort((a, b) => a.id.localeCompare(b.id));
175
+ // Debug trace in development mode
176
+ if (process.env.NODE_ENV === 'development') {
177
+ logger.debug(`Built ${rootNodes.length} root CloudDOM nodes from ${cloudDOMNodes.length} total nodes`);
178
+ logger.debug('Tree:', JSON.stringify(rootNodes, null, 2));
179
+ }
180
+ // Lifecycle hook: afterBuild (supports async, isolated errors)
181
+ if (this.afterBuildHook) {
182
+ try {
183
+ await this.afterBuildHook(rootNodes);
184
+ }
185
+ catch (err) {
186
+ logger.error('afterBuild hook failed (non-critical):', err);
187
+ }
188
+ }
189
+ return rootNodes;
190
+ }
191
+ /**
192
+ * Build CloudDOM tree with error handling for CLI/CI environments
193
+ *
194
+ * Provides a safer entrypoint that handles errors gracefully without
195
+ * crashing the entire process. Useful for CI/CD pipelines.
196
+ *
197
+ * @param fiber - Root Fiber node
198
+ * @returns Promise resolving to array of CloudDOM nodes, or empty array on error
199
+ */
200
+ async buildSafe(fiber) {
201
+ try {
202
+ return await this.build(fiber);
203
+ }
204
+ catch (error) {
205
+ logger.error('Build failed:', error);
206
+ return [];
207
+ }
208
+ }
209
+ /**
210
+ * Recursively collect CloudDOM nodes from Fiber tree
211
+ *
212
+ * Traverses the Fiber tree depth-first and collects all nodes that have
213
+ * cloudDOMNode or cloudDOMNodes attached (i.e., components that called useInstance).
214
+ *
215
+ * Normalizes paths and IDs during collection for consistency.
216
+ *
217
+ * @param fiber - Current Fiber node
218
+ * @param collected - Array to collect CloudDOM nodes into
219
+ */
220
+ collectCloudDOMNodes(fiber, collected) {
221
+ // Extract state from useState hooks in this Fiber node
222
+ const stateValues = this.extractOutputsFromFiber(fiber);
223
+ // Check for cloudDOMNodes array (multiple nodes from useInstance calls)
224
+ if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
225
+ for (const cloudNode of fiber.cloudDOMNodes) {
226
+ if (this.isValidCloudNode(cloudNode)) {
227
+ // Normalize path and regenerate ID for consistency
228
+ cloudNode.path = (0, naming_1.normalizePath)(cloudNode.path);
229
+ cloudNode.id = (0, naming_1.generateResourceId)(cloudNode.path);
230
+ // Attach state from useState hooks to CloudDOM node's state field
231
+ if (Object.keys(stateValues).length > 0) {
232
+ cloudNode.state = { ...stateValues };
233
+ }
234
+ collected.push(cloudNode);
235
+ }
236
+ else {
237
+ const nodeId = cloudNode?.id ?? 'unknown';
238
+ logger.warn(`Skipping invalid CloudDOM node: ${nodeId}`);
239
+ }
240
+ }
241
+ }
242
+ // Legacy cloudDOMNode removed - only cloudDOMNodes array is supported
243
+ // Recursively collect from children
244
+ if (fiber.children && fiber.children.length > 0) {
245
+ for (const child of fiber.children) {
246
+ this.collectCloudDOMNodes(child, collected);
247
+ }
248
+ }
249
+ }
250
+ /**
251
+ * Extract state from useState hooks in a Fiber node
252
+ *
253
+ * REQ-02: Extract outputs from useState calls
254
+ * REQ-06: Universal output access
255
+ * REQ-2.1: Store useState values without 'state.' prefix
256
+ *
257
+ * @param fiber - Fiber node to extract state from
258
+ * @returns Object mapping state keys to values (state1, state2, etc.)
259
+ */
260
+ extractOutputsFromFiber(fiber) {
261
+ const stateValues = {};
262
+ // Debug logging
263
+ if (process.env.CREACT_DEBUG === 'true') {
264
+ const safeStringify = (value) => {
265
+ try {
266
+ return JSON.stringify(value);
267
+ }
268
+ catch {
269
+ return String(value);
270
+ }
271
+ };
272
+ logger.debug(`extractOutputsFromFiber: fiber.path=${fiber.path?.join('.')}, hooks=${safeStringify(fiber.hooks)}`);
273
+ }
274
+ // Check if this Fiber has hooks from useState calls
275
+ if (fiber.hooks && Array.isArray(fiber.hooks) && fiber.hooks.length > 0) {
276
+ // Each hook in the array represents a useState call
277
+ // REQ-2.1: Store as state1, state2, etc. (no 'state.' prefix)
278
+ fiber.hooks.forEach((hookValue, index) => {
279
+ // Generate state key without prefix: state1, state2, etc.
280
+ const stateKey = `state${index + 1}`;
281
+ stateValues[stateKey] = hookValue;
282
+ });
283
+ if (process.env.CREACT_DEBUG === 'true') {
284
+ logger.debug(`Extracted useState values without prefix:`, stateValues);
285
+ }
286
+ }
287
+ return stateValues;
288
+ }
289
+ /**
290
+ * Execute post-deployment effects for all Fiber nodes
291
+ * Called after successful deployment to run useEffect callbacks
292
+ *
293
+ * @param fiber - Root Fiber node
294
+ */
295
+ async executePostDeploymentEffects(fiber) {
296
+ const { executeEffects } = await Promise.resolve().then(() => __importStar(require('../hooks/useEffect')));
297
+ logger.debug(`Executing effects for fiber: ${fiber.path?.join('.')}`);
298
+ logger.debug(`Fiber has effects: ${!!fiber.effects}`);
299
+ // Execute effects for this node
300
+ executeEffects(fiber);
301
+ // Recursively execute effects for children
302
+ if (fiber.children && fiber.children.length > 0) {
303
+ for (const child of fiber.children) {
304
+ await this.executePostDeploymentEffects(child);
305
+ }
306
+ }
307
+ }
308
+ /**
309
+ * Sync Fiber hook state back to CloudDOM outputs after effects run
310
+ * This ensures useState changes in useEffect are reflected in the CloudDOM
311
+ *
312
+ * @param fiber - Root Fiber node
313
+ * @param cloudDOM - CloudDOM nodes to update
314
+ */
315
+ syncFiberStateToCloudDOM(fiber, cloudDOM) {
316
+ logger.debug(`Syncing Fiber state to CloudDOM...`);
317
+ // Build a map of CloudDOM nodes by path for fast lookup
318
+ const cloudDOMMap = new Map();
319
+ const buildMap = (nodes) => {
320
+ for (const node of nodes) {
321
+ const pathKey = node.path.join('.');
322
+ cloudDOMMap.set(pathKey, node);
323
+ if (node.children && node.children.length > 0) {
324
+ buildMap(node.children);
325
+ }
326
+ }
327
+ };
328
+ buildMap(cloudDOM);
329
+ // Recursively sync Fiber state to CloudDOM
330
+ this.syncFiberNodeToCloudDOM(fiber, cloudDOMMap);
331
+ logger.debug(`Fiber state sync completed`);
332
+ }
333
+ /**
334
+ * Sync a single Fiber node's state to its corresponding CloudDOM node
335
+ */
336
+ syncFiberNodeToCloudDOM(fiber, cloudDOMMap) {
337
+ // Check if this Fiber node has corresponding CloudDOM nodes
338
+ if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
339
+ for (const cloudNode of fiber.cloudDOMNodes) {
340
+ this.updateCloudDOMNodeOutputs(fiber, cloudNode);
341
+ }
342
+ }
343
+ // Recursively sync children
344
+ if (fiber.children && fiber.children.length > 0) {
345
+ for (const child of fiber.children) {
346
+ this.syncFiberNodeToCloudDOM(child, cloudDOMMap);
347
+ }
348
+ }
349
+ }
350
+ /**
351
+ * Update a CloudDOM node's state with the latest Fiber hook state
352
+ *
353
+ * REQ-1.2, 1.3: Separate useState values from provider outputs
354
+ * State is stored in separate `state` field, not in `outputs`
355
+ */
356
+ updateCloudDOMNodeOutputs(fiber, cloudNode) {
357
+ // Extract fresh state values from the Fiber node
358
+ const stateValues = this.extractOutputsFromFiber(fiber);
359
+ if (Object.keys(stateValues).length > 0) {
360
+ // REQ-1.2: Write to state field, not outputs
361
+ cloudNode.state = stateValues;
362
+ logger.debug(`Synced state for ${cloudNode.id}:`, cloudNode.state);
363
+ }
364
+ // CRITICAL FIX: Sync state back to original nodes that components reference
365
+ this.syncOutputsToOriginalNodes(fiber, cloudNode);
366
+ }
367
+ /**
368
+ * Sync state back to the original CloudDOM nodes that components reference
369
+ * This ensures that enhanced node proxies see the updated state
370
+ */
371
+ syncOutputsToOriginalNodes(fiber, updatedNode) {
372
+ // Find the original nodes in the fiber that match this updated node
373
+ if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
374
+ for (const originalNode of fiber.cloudDOMNodes) {
375
+ if (originalNode.id === updatedNode.id) {
376
+ // Update the original node's state with the latest values
377
+ if (updatedNode.state) {
378
+ originalNode.state = { ...updatedNode.state };
379
+ if (process.env.CREACT_DEBUG === 'true') {
380
+ logger.debug(`Synced state to original node ${originalNode.id}:`, originalNode.state);
381
+ }
382
+ }
383
+ break;
384
+ }
385
+ }
386
+ }
387
+ // Legacy cloudDOMNode removed
388
+ // Recursively sync for child fibers
389
+ if (fiber.children && fiber.children.length > 0) {
390
+ for (const child of fiber.children) {
391
+ this.syncOutputsToOriginalNodes(child, updatedNode);
392
+ }
393
+ }
394
+ }
395
+ /**
396
+ * Type guard to validate CloudDOM node structure
397
+ *
398
+ * @param node - Potential CloudDOM node
399
+ * @returns True if node is a valid CloudDOM node
400
+ */
401
+ isValidCloudNode(node) {
402
+ return (typeof node?.id === 'string' &&
403
+ Array.isArray(node?.path) &&
404
+ node.path.length > 0 &&
405
+ typeof node?.construct !== 'undefined' &&
406
+ typeof node?.props === 'object' &&
407
+ Array.isArray(node?.children));
408
+ }
409
+ /**
410
+ * Validate CloudDOM nodes for common issues
411
+ *
412
+ * Checks for:
413
+ * - Duplicate IDs (would cause silent overwrites in hierarchy building)
414
+ * - Invalid paths (empty or non-array)
415
+ * - Circular references in paths
416
+ *
417
+ * Enhanced for re-render scenarios:
418
+ * - Allows re-validation of existing nodes from the same fiber path
419
+ * - Prevents false positives during reactive re-renders
420
+ *
421
+ * @param nodes - CloudDOM nodes to validate
422
+ * @throws Error if duplicate IDs or circular references are found
423
+ */
424
+ validateCloudDOMNodes(nodes) {
425
+ const seenIds = new Set();
426
+ const pathStrings = new Set();
427
+ for (const node of nodes) {
428
+ const currentFiberPath = node.path.join('.');
429
+ // Check for duplicate IDs with re-render support
430
+ if (seenIds.has(node.id)) {
431
+ // Check if this is a re-render of the same node from the same fiber path
432
+ const existingFiberPath = this.existingNodeMap.get(node.id);
433
+ if (existingFiberPath === currentFiberPath) {
434
+ // This is a re-render of the same node - allow it
435
+ if (process.env.CREACT_DEBUG === 'true') {
436
+ logger.debug(`Re-validating existing node during re-render: ${node.id}`);
437
+ }
438
+ continue;
439
+ }
440
+ // This is a true duplicate from different fiber paths
441
+ throw new Error(`[CloudDOMBuilder] Duplicate CloudDOMNode id detected: '${node.id}' at ${this.formatPath(node)}. ` +
442
+ `Each resource must have a unique ID. ` +
443
+ `Use the 'key' prop to differentiate components with the same name.`);
444
+ }
445
+ seenIds.add(node.id);
446
+ // Track this node for future re-render validation
447
+ this.existingNodeMap.set(node.id, currentFiberPath);
448
+ // Validate path (should already be filtered in collectCloudDOMNodes, but double-check)
449
+ if (!Array.isArray(node.path) || node.path.length === 0) {
450
+ throw new Error(`[CloudDOMBuilder] CloudDOMNode '${node.id}' has invalid path. ` +
451
+ `Path must be a non-empty array of strings.`);
452
+ }
453
+ // Check for circular references (path pointing to itself)
454
+ const pathString = node.path.join('.');
455
+ if (pathStrings.has(pathString)) {
456
+ // Multiple nodes with same path could indicate circular reference
457
+ const existingNode = nodes.find((n) => n.path.join('.') === pathString && n.id !== node.id);
458
+ if (existingNode) {
459
+ throw new Error(`[CloudDOMBuilder] Circular dependency detected: nodes '${existingNode.id}' and '${node.id}' share the same path '${pathString}'`);
460
+ }
461
+ }
462
+ pathStrings.add(pathString);
463
+ }
464
+ }
465
+ /**
466
+ * Create deep defensive copy of CloudDOM nodes to avoid mutation
467
+ *
468
+ * Recursively copies:
469
+ * - Node properties
470
+ * - Props object
471
+ * - Outputs object
472
+ * - Children array (deep copy)
473
+ *
474
+ * This prevents mutations from later phases (e.g., provider injection,
475
+ * dependency graph tagging) from affecting the original nodes.
476
+ *
477
+ * @param nodes - Original CloudDOM nodes
478
+ * @returns Deep copy of nodes
479
+ */
480
+ createDeepDefensiveCopy(nodes) {
481
+ return nodes.map((node) => ({
482
+ ...node,
483
+ props: { ...node.props },
484
+ outputs: node.outputs ? { ...node.outputs } : undefined,
485
+ children: node.children ? this.createDeepDefensiveCopy(node.children) : [],
486
+ }));
487
+ }
488
+ /**
489
+ * Build parent-child hierarchy from flat list of CloudDOM nodes
490
+ *
491
+ * Uses the path property to determine parent-child relationships:
492
+ * - A node is a child of another if its path starts with the parent's path
493
+ * - Returns only root nodes (nodes with no parent)
494
+ *
495
+ * Example:
496
+ * - ['registry'] is root
497
+ * - ['registry', 'service'] is child of ['registry']
498
+ * - ['registry', 'service', 'task'] is child of ['registry', 'service']
499
+ *
500
+ * Optimization: Groups nodes by depth for O(n) parent lookup instead of O(n²)
501
+ *
502
+ * @param nodes - Flat array of CloudDOM nodes
503
+ * @returns Array of root CloudDOM nodes with children attached
504
+ */
505
+ buildHierarchy(nodes) {
506
+ if (nodes.length === 0) {
507
+ return [];
508
+ }
509
+ // Group nodes by depth (path length) for optimized parent lookup
510
+ const nodesByDepth = new Map();
511
+ for (const node of nodes) {
512
+ const depth = node.path.length;
513
+ if (!nodesByDepth.has(depth)) {
514
+ nodesByDepth.set(depth, []);
515
+ }
516
+ nodesByDepth.get(depth).push(node);
517
+ }
518
+ // Find root nodes and build parent-child relationships
519
+ const rootNodes = [];
520
+ for (const node of nodes) {
521
+ // Find parent by checking nodes at depth - 1
522
+ const parentNode = this.findParentOptimized(node, nodesByDepth);
523
+ if (parentNode) {
524
+ // Add this node as a child of its parent
525
+ if (!parentNode.children) {
526
+ parentNode.children = [];
527
+ }
528
+ parentNode.children.push(node);
529
+ }
530
+ else {
531
+ // No parent found - this is a root node
532
+ rootNodes.push(node);
533
+ }
534
+ }
535
+ return rootNodes;
536
+ }
537
+ /**
538
+ * Find the parent node for a given node (optimized version)
539
+ *
540
+ * A node is the parent if:
541
+ * 1. Its path is a prefix of the child's path
542
+ * 2. Its path length is exactly one less than the child's path length (immediate parent)
543
+ *
544
+ * Example:
545
+ * - Child path: ['registry', 'service', 'task']
546
+ * - Parent path: ['registry', 'service'] ✓ (immediate parent)
547
+ * - Not parent: ['registry'] ✗ (grandparent, not immediate)
548
+ *
549
+ * Optimization: Uses nodesByDepth map to only search nodes at parent depth (O(n) instead of O(n²))
550
+ *
551
+ * @param node - Node to find parent for
552
+ * @param nodesByDepth - Map of nodes grouped by path depth
553
+ * @returns Parent node or undefined if no parent
554
+ */
555
+ findParentOptimized(node, nodesByDepth) {
556
+ // Root nodes have no parent
557
+ if (node.path.length === 1) {
558
+ return undefined;
559
+ }
560
+ // Look for immediate parent at depth - 1
561
+ const parentDepth = node.path.length - 1;
562
+ const possibleParents = nodesByDepth.get(parentDepth);
563
+ if (!possibleParents) {
564
+ // No nodes at parent depth - this is an orphan node
565
+ if (process.env.NODE_ENV === 'development') {
566
+ logger.warn(`Orphaned node '${node.id}' (parent missing for path ${node.path.join('.')})`);
567
+ }
568
+ return undefined;
569
+ }
570
+ // Find parent by checking if path is a prefix
571
+ return possibleParents.find((parent) => parent.path.every((segment, i) => segment === node.path[i]));
572
+ }
573
+ /**
574
+ * Get the cloud provider (for testing/debugging)
575
+ *
576
+ * @returns The injected cloud provider
577
+ */
578
+ getCloudProvider() {
579
+ return this.cloudProvider;
580
+ }
581
+ /**
582
+ * Convert CloudDOM tree to flat map for debugging and backend storage
583
+ *
584
+ * Useful for:
585
+ * - Backend state providers that need flat ID → node mapping
586
+ * - Debugging and inspection
587
+ * - Quick lookups by ID
588
+ *
589
+ * Type-safe: Preserves node subtype information for provider-specific extensions
590
+ *
591
+ * @param rootNodes - Root CloudDOM nodes
592
+ * @returns Flat map of ID → CloudDOM node
593
+ */
594
+ toFlatMap(rootNodes) {
595
+ const map = {};
596
+ const walk = (node) => {
597
+ map[node.id] = node;
598
+ if (node.children && node.children.length > 0) {
599
+ node.children.forEach((child) => walk(child));
600
+ }
601
+ };
602
+ rootNodes.forEach(walk);
603
+ return map;
604
+ }
605
+ /**
606
+ * Normalize path segments for consistent resource addressing
607
+ *
608
+ * Ensures consistent casing and formatting across environments:
609
+ * - Trims whitespace
610
+ * - Converts to lowercase
611
+ * - Replaces slashes and spaces with hyphens
612
+ *
613
+ * REQ-NF: Future safety for cross-OS consistency
614
+ *
615
+ * @param path - Path segments to normalize
616
+ * @returns Normalized path segments
617
+ */
618
+ normalizePath(path) {
619
+ return path.map((segment) => segment
620
+ .trim()
621
+ .toLowerCase()
622
+ .replace(/[\/\s]+/g, '-')
623
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
624
+ );
625
+ }
626
+ /**
627
+ * Format path for human-readable error messages
628
+ *
629
+ * @param node - CloudDOM node
630
+ * @returns Formatted path string (e.g., "registry > service > task")
631
+ */
632
+ formatPath(node) {
633
+ return node.path.join(' > ');
634
+ }
635
+ /**
636
+ * Detect circular references in CloudDOM hierarchy
637
+ *
638
+ * Uses depth-first search to detect cycles in parent-child relationships.
639
+ * This is a graph-level safety check beyond simple path validation.
640
+ *
641
+ * @param nodes - Root CloudDOM nodes
642
+ * @throws Error if circular hierarchy is detected
643
+ */
644
+ detectCircularRefs(nodes) {
645
+ const visited = new Set();
646
+ const stack = new Set();
647
+ const dfs = (node) => {
648
+ if (stack.has(node.id)) {
649
+ throw new Error(`[CloudDOMBuilder] Circular hierarchy detected at '${node.id}' (path: ${this.formatPath(node)})`);
650
+ }
651
+ if (visited.has(node.id)) {
652
+ return;
653
+ }
654
+ visited.add(node.id);
655
+ stack.add(node.id);
656
+ if (node.children && node.children.length > 0) {
657
+ node.children.forEach(dfs);
658
+ }
659
+ stack.delete(node.id);
660
+ };
661
+ nodes.forEach(dfs);
662
+ }
663
+ /**
664
+ * Build efficient node map for CloudDOM comparison
665
+ * Creates a flat map of node ID to node for fast lookup during comparison
666
+ * Handles nested node structures properly
667
+ *
668
+ * REQ-1.4, 9.1: Efficient map building for CloudDOM comparison
669
+ *
670
+ * @param nodes - CloudDOM nodes to map
671
+ * @returns Map of node ID to CloudDOM node
672
+ */
673
+ buildNodeMap(nodes) {
674
+ const nodeMap = new Map();
675
+ const walkNodes = (nodeList) => {
676
+ for (const node of nodeList) {
677
+ // Ensure consistent node identification
678
+ if (node.id) {
679
+ nodeMap.set(node.id, node);
680
+ }
681
+ // Recursively handle nested children
682
+ if (node.children && node.children.length > 0) {
683
+ walkNodes(node.children);
684
+ }
685
+ }
686
+ };
687
+ walkNodes(nodes);
688
+ return nodeMap;
689
+ }
690
+ /**
691
+ * Detect output changes after deployment by comparing previous and current CloudDOM
692
+ * This is called after deployment to identify which provider outputs have changed
693
+ *
694
+ * Enhanced to:
695
+ * - Compare previous and current CloudDOM states properly
696
+ * - Identify specific output keys that changed
697
+ * - Track affected fibers for each change
698
+ *
699
+ * REQ-1.4, 1.5, 9.1, 9.2, 9.3: Enhanced output change detection
700
+ *
701
+ * @param previous - Previous CloudDOM state
702
+ * @param current - Current CloudDOM state after deployment
703
+ * @returns Array of output changes detected
704
+ */
705
+ detectOutputChanges(previous, current) {
706
+ const changes = [];
707
+ // REQ-9.1: Build efficient maps for comparison using buildNodeMap
708
+ const previousMap = this.buildNodeMap(previous);
709
+ const currentMap = this.buildNodeMap(current);
710
+ if (process.env.CREACT_DEBUG === 'true') {
711
+ logger.debug(`Detecting output changes: ${previousMap.size} previous nodes, ${currentMap.size} current nodes`);
712
+ }
713
+ // REQ-9.2, 9.3: Compare previous and current states, identify specific changes
714
+ // Check for changed outputs in existing nodes and new outputs in new nodes
715
+ for (const [nodeId, currentNode] of currentMap.entries()) {
716
+ const previousNode = previousMap.get(nodeId);
717
+ // REQ-9.3: Track affected fibers for each change
718
+ const getAffectedFibers = () => {
719
+ if (!this.providerOutputTracker) {
720
+ return [];
721
+ }
722
+ return Array.from(this.providerOutputTracker.getBindingsForInstance(nodeId));
723
+ };
724
+ if (previousNode) {
725
+ // Node existed before - check for changed or new outputs
726
+ const currentOutputs = currentNode.outputs || {};
727
+ const previousOutputs = previousNode.outputs || {};
728
+ // Check for changed or new outputs
729
+ for (const [outputKey, currentValue] of Object.entries(currentOutputs)) {
730
+ const previousValue = previousOutputs[outputKey];
731
+ // REQ-9.2: Identify specific output keys that changed
732
+ if (previousValue !== currentValue) {
733
+ changes.push({
734
+ nodeId,
735
+ outputKey,
736
+ previousValue,
737
+ newValue: currentValue,
738
+ affectedFibers: getAffectedFibers(),
739
+ });
740
+ if (process.env.CREACT_DEBUG === 'true') {
741
+ logger.debug(`Output changed: ${nodeId}.${outputKey} (${previousValue} → ${currentValue})`);
742
+ }
743
+ }
744
+ }
745
+ // Check for removed outputs
746
+ for (const [outputKey, previousValue] of Object.entries(previousOutputs)) {
747
+ if (!(outputKey in currentOutputs)) {
748
+ changes.push({
749
+ nodeId,
750
+ outputKey,
751
+ previousValue,
752
+ newValue: undefined,
753
+ affectedFibers: getAffectedFibers(),
754
+ });
755
+ if (process.env.CREACT_DEBUG === 'true') {
756
+ logger.debug(`Output removed: ${nodeId}.${outputKey}`);
757
+ }
758
+ }
759
+ }
760
+ }
761
+ else {
762
+ // New node - all outputs are new
763
+ const currentOutputs = currentNode.outputs || {};
764
+ for (const [outputKey, currentValue] of Object.entries(currentOutputs)) {
765
+ changes.push({
766
+ nodeId,
767
+ outputKey,
768
+ previousValue: undefined,
769
+ newValue: currentValue,
770
+ affectedFibers: getAffectedFibers(),
771
+ });
772
+ if (process.env.CREACT_DEBUG === 'true') {
773
+ logger.debug(`New output: ${nodeId}.${outputKey} = ${currentValue}`);
774
+ }
775
+ }
776
+ }
777
+ }
778
+ // Check for deleted nodes (all their outputs are removed)
779
+ for (const [nodeId, previousNode] of previousMap.entries()) {
780
+ if (!currentMap.has(nodeId)) {
781
+ const previousOutputs = previousNode.outputs || {};
782
+ const getAffectedFibers = () => {
783
+ if (!this.providerOutputTracker) {
784
+ return [];
785
+ }
786
+ return Array.from(this.providerOutputTracker.getBindingsForInstance(nodeId));
787
+ };
788
+ for (const [outputKey, previousValue] of Object.entries(previousOutputs)) {
789
+ changes.push({
790
+ nodeId,
791
+ outputKey,
792
+ previousValue,
793
+ newValue: undefined,
794
+ affectedFibers: getAffectedFibers(),
795
+ });
796
+ if (process.env.CREACT_DEBUG === 'true') {
797
+ logger.debug(`Node deleted, output removed: ${nodeId}.${outputKey}`);
798
+ }
799
+ }
800
+ }
801
+ }
802
+ if (process.env.CREACT_DEBUG === 'true') {
803
+ logger.debug(`Detected ${changes.length} output changes`);
804
+ }
805
+ return changes;
806
+ }
807
+ /**
808
+ * Synchronize provider outputs to bound state and trigger re-renders
809
+ * This is the main method called after deployment to update reactive state
810
+ *
811
+ * @param fiber - Root fiber node
812
+ * @param cloudDOM - Current CloudDOM after deployment
813
+ * @param previousCloudDOM - Previous CloudDOM state (optional)
814
+ * @returns Promise resolving to affected fibers that need re-rendering
815
+ */
816
+ async syncOutputsAndReRender(fiber, cloudDOM, previousCloudDOM) {
817
+ if (!this.stateBindingManager || !this.providerOutputTracker) {
818
+ logger.warn('Reactive components not set, skipping output sync');
819
+ return [];
820
+ }
821
+ try {
822
+ // Step 1: Update provider output tracker with new outputs
823
+ // Note: Output syncing to original nodes is now done before effects in integrateWithPostDeploymentEffects
824
+ const outputChanges = [];
825
+ for (const node of cloudDOM) {
826
+ if (node.outputs) {
827
+ const nodeChanges = this.providerOutputTracker.updateInstanceOutputs(node.id, node.outputs);
828
+ outputChanges.push(...nodeChanges);
829
+ }
830
+ }
831
+ // Step 2: If we have previous state, detect additional changes
832
+ if (previousCloudDOM) {
833
+ const additionalChanges = this.detectOutputChanges(previousCloudDOM, cloudDOM);
834
+ // Merge changes, avoiding duplicates
835
+ for (const change of additionalChanges) {
836
+ const exists = outputChanges.some((existing) => existing.nodeId === change.nodeId && existing.outputKey === change.outputKey);
837
+ if (!exists) {
838
+ outputChanges.push(change);
839
+ }
840
+ }
841
+ }
842
+ // Step 3: Apply output changes to bound state
843
+ const stateAffectedFibers = this.applyOutputChangesToState(outputChanges);
844
+ // Step 4: Update context provider values (NEW - enables context reactivity)
845
+ // This re-evaluates context provider values that depend on outputs
846
+ console.log('[CONTEXT REACTIVITY] Updating context provider values after output changes...');
847
+ const contextAffectedFibers = this.updateContextProviderValues(fiber);
848
+ // Merge affected fibers from both state and context updates
849
+ const allAffectedFibers = [...stateAffectedFibers];
850
+ for (const contextFiber of contextAffectedFibers) {
851
+ if (!allAffectedFibers.includes(contextFiber)) {
852
+ allAffectedFibers.push(contextFiber);
853
+ }
854
+ }
855
+ // Debug logging
856
+ console.log(`[CONTEXT REACTIVITY] Synced ${outputChanges.length} output changes`);
857
+ console.log(`[CONTEXT REACTIVITY] State affected: ${stateAffectedFibers.length} fibers`);
858
+ logger.debug(` Context affected: ${contextAffectedFibers.length} fibers`);
859
+ logger.debug(` Total affected: ${allAffectedFibers.length} fibers`);
860
+ // NEW (Gap 2): Flush ReactiveUpdateQueue to get all dirty fibers
861
+ // This includes fibers enqueued by applyOutputChangesToState
862
+ const reactiveQueue = (0, ReactiveUpdateQueue_1.getReactiveUpdateQueue)();
863
+ const queuedFibers = reactiveQueue.flush();
864
+ if (queuedFibers.length > 0) {
865
+ logger.debug(`Flushed ${queuedFibers.length} fibers from ReactiveUpdateQueue`);
866
+ // Merge queued fibers with affected fibers (avoid duplicates)
867
+ for (const queuedFiber of queuedFibers) {
868
+ if (!allAffectedFibers.includes(queuedFiber)) {
869
+ allAffectedFibers.push(queuedFiber);
870
+ }
871
+ }
872
+ }
873
+ logger.debug(`Total affected fibers after queue flush: ${allAffectedFibers.length}`);
874
+ return allAffectedFibers;
875
+ }
876
+ catch (error) {
877
+ logger.error('Error during output synchronization:', error);
878
+ return [];
879
+ }
880
+ }
881
+ /**
882
+ * Sync CloudDOM outputs to the original nodes that components reference
883
+ * This is critical for reactivity to work - components need to see updated outputs
884
+ */
885
+ syncCloudDOMOutputsToOriginalNodes(fiber, cloudDOM) {
886
+ logger.debug('syncCloudDOMOutputsToOriginalNodes called');
887
+ logger.debug(`CloudDOM nodes to sync: ${cloudDOM.length}`);
888
+ // Build a map of CloudDOM nodes by ID for fast lookup
889
+ const cloudDOMMap = new Map();
890
+ const buildMap = (nodes) => {
891
+ for (const node of nodes) {
892
+ cloudDOMMap.set(node.id, node);
893
+ if (node.outputs && Object.keys(node.outputs).length > 0) {
894
+ logger.debug(`CloudDOM node ${node.id} has outputs:`, Object.keys(node.outputs));
895
+ }
896
+ if (node.children && node.children.length > 0) {
897
+ buildMap(node.children);
898
+ }
899
+ }
900
+ };
901
+ buildMap(cloudDOM);
902
+ logger.debug(`Built CloudDOM map with ${cloudDOMMap.size} nodes`);
903
+ // Recursively sync outputs to original nodes
904
+ this.syncOutputsToOriginalNodesRecursive(fiber, cloudDOMMap);
905
+ }
906
+ /**
907
+ * Recursively sync outputs to original nodes in the fiber tree
908
+ */
909
+ syncOutputsToOriginalNodesRecursive(fiber, cloudDOMMap) {
910
+ // Sync outputs for nodes in this fiber
911
+ if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
912
+ for (const originalNode of fiber.cloudDOMNodes) {
913
+ const updatedNode = cloudDOMMap.get(originalNode.id);
914
+ if (updatedNode && updatedNode.outputs) {
915
+ if (!originalNode.outputs) {
916
+ originalNode.outputs = {};
917
+ }
918
+ // Merge outputs from the deployed CloudDOM back to the original node
919
+ const beforeOutputs = { ...originalNode.outputs };
920
+ Object.assign(originalNode.outputs, updatedNode.outputs);
921
+ logger.debug(`✓ Synced outputs to original node ${originalNode.id}`);
922
+ logger.debug(` Before:`, beforeOutputs);
923
+ logger.debug(` After:`, originalNode.outputs);
924
+ }
925
+ }
926
+ }
927
+ // Legacy cloudDOMNode removed
928
+ // Recursively sync for child fibers
929
+ if (fiber.children && fiber.children.length > 0) {
930
+ for (const child of fiber.children) {
931
+ this.syncOutputsToOriginalNodesRecursive(child, cloudDOMMap);
932
+ }
933
+ }
934
+ }
935
+ /**
936
+ * Update context provider values when outputs change
937
+ * This enables context reactivity by finding components that create context values from outputs
938
+ *
939
+ * Strategy:
940
+ * 1. Find all fibers that have both cloudDOMNodes (use outputs) AND render a Context.Provider child
941
+ * 2. These are the components that create context values from outputs
942
+ * 3. Mark them for re-render so they re-evaluate their context values
943
+ *
944
+ * @param fiber - Root fiber to search
945
+ * @returns Array of fibers that need re-rendering (components creating context values from outputs)
946
+ */
947
+ updateContextProviderValues(fiber) {
948
+ const affectedFibers = [];
949
+ // Recursively find components that create context values from outputs
950
+ this.findContextValueCreators(fiber, affectedFibers);
951
+ return affectedFibers;
952
+ }
953
+ /**
954
+ * Recursively find components that create context values from outputs
955
+ * These are components that:
956
+ * 1. Have cloudDOMNodes (use useInstance - depend on outputs)
957
+ * 2. Have children that are Context.Provider components
958
+ */
959
+ findContextValueCreators(fiber, affectedFibers) {
960
+ // Check if this fiber uses outputs (has cloudDOMNodes)
961
+ const usesOutputs = fiber.cloudDOMNodes && fiber.cloudDOMNodes.length > 0;
962
+ // Check if this fiber has Context.Provider children
963
+ const hasProviderChild = fiber.children &&
964
+ fiber.children.some((child) => typeof child.type === 'function' && child.type._isContextProvider);
965
+ logger.info(`[Context Reactivity] Checking fiber: ${fiber.path?.join('.')}`);
966
+ logger.info(` Uses outputs: ${usesOutputs}`);
967
+ logger.info(` Has provider child: ${hasProviderChild}`);
968
+ if (usesOutputs && hasProviderChild) {
969
+ // This component creates context values from outputs - needs re-render
970
+ logger.info(`[Context Reactivity] ✓ Found context value creator: ${fiber.path?.join('.')}`);
971
+ logger.info(` Has ${fiber.cloudDOMNodes?.length || 0} output dependencies`);
972
+ // Add to affected fibers for re-rendering
973
+ if (!affectedFibers.includes(fiber)) {
974
+ affectedFibers.push(fiber);
975
+ }
976
+ }
977
+ // Recursively process children
978
+ if (fiber.children && fiber.children.length > 0) {
979
+ for (const child of fiber.children) {
980
+ this.findContextValueCreators(child, affectedFibers);
981
+ }
982
+ }
983
+ }
984
+ /**
985
+ * Apply output changes to bound state using StateBindingManager
986
+ * This updates useState values that are bound to provider outputs
987
+ *
988
+ * Enhanced to:
989
+ * - Apply detected changes to bound state via StateBindingManager
990
+ * - Handle binding updates without creating loops
991
+ * - Return affected fibers for re-rendering
992
+ *
993
+ * REQ-9.4, 9.5: Apply output changes to state and return affected fibers
994
+ *
995
+ * @param changes - Array of output changes to apply
996
+ * @returns Array of fibers affected by the changes
997
+ */
998
+ applyOutputChangesToState(changes) {
999
+ if (!this.stateBindingManager) {
1000
+ if (process.env.CREACT_DEBUG === 'true') {
1001
+ logger.debug('No StateBindingManager available, skipping state updates');
1002
+ }
1003
+ return [];
1004
+ }
1005
+ if (changes.length === 0) {
1006
+ if (process.env.CREACT_DEBUG === 'true') {
1007
+ logger.debug('No output changes to apply');
1008
+ }
1009
+ return [];
1010
+ }
1011
+ try {
1012
+ if (process.env.CREACT_DEBUG === 'true') {
1013
+ logger.debug(`Applying ${changes.length} output changes to bound state`);
1014
+ }
1015
+ // REQ-9.4: Process changes through state binding manager
1016
+ // This uses internal setState to prevent circular dependencies (REQ-4.2, 4.4)
1017
+ const affectedFibers = this.stateBindingManager.processOutputChanges(changes);
1018
+ if (process.env.CREACT_DEBUG === 'true') {
1019
+ logger.debug(`${affectedFibers.length} fibers affected by output changes`);
1020
+ }
1021
+ // Notify provider output tracker about the changes for tracking
1022
+ if (this.providerOutputTracker) {
1023
+ try {
1024
+ this.providerOutputTracker.notifyOutputChanges(changes);
1025
+ }
1026
+ catch (error) {
1027
+ logger.warn('Error notifying provider output tracker:', error);
1028
+ // Continue despite notification failure
1029
+ }
1030
+ }
1031
+ // NEW (Gap 2): Enqueue affected fibers to ReactiveUpdateQueue
1032
+ // This batches re-renders and prevents duplicate scheduling
1033
+ const reactiveQueue = (0, ReactiveUpdateQueue_1.getReactiveUpdateQueue)();
1034
+ affectedFibers.forEach((fiber) => {
1035
+ reactiveQueue.enqueue(fiber);
1036
+ });
1037
+ if (process.env.CREACT_DEBUG === 'true') {
1038
+ logger.debug(`Enqueued ${affectedFibers.length} fibers to ReactiveUpdateQueue`);
1039
+ }
1040
+ // Execute reactive effects for affected fibers
1041
+ // This triggers useEffect callbacks that depend on changed outputs
1042
+ this.executeReactiveEffects(affectedFibers, changes);
1043
+ // REQ-9.5: Return affected fibers for re-rendering
1044
+ return affectedFibers;
1045
+ }
1046
+ catch (error) {
1047
+ logger.error('Error applying output changes to state:', error);
1048
+ // Return empty array to prevent re-render failures from blocking deployment
1049
+ // The error has been logged for debugging
1050
+ return [];
1051
+ }
1052
+ }
1053
+ /**
1054
+ * Execute reactive effects for fibers affected by output changes
1055
+ * This triggers useEffect callbacks that depend on changed provider outputs
1056
+ *
1057
+ * @param affectedFibers - Fibers that were affected by output changes
1058
+ * @param changes - Output changes that occurred
1059
+ */
1060
+ async executeReactiveEffects(affectedFibers, changes) {
1061
+ if (affectedFibers.length === 0) {
1062
+ return;
1063
+ }
1064
+ try {
1065
+ const { executeEffectsOnOutputChange } = await Promise.resolve().then(() => __importStar(require('../hooks/useEffect')));
1066
+ const { generateBindingKey } = await Promise.resolve().then(() => __importStar(require('../utils/naming')));
1067
+ // Convert output changes to binding keys for effect matching
1068
+ const changedOutputKeys = new Set();
1069
+ for (const change of changes) {
1070
+ const bindingKey = generateBindingKey(change.nodeId, change.outputKey);
1071
+ changedOutputKeys.add(bindingKey);
1072
+ }
1073
+ // Execute reactive effects for each affected fiber
1074
+ for (const fiber of affectedFibers) {
1075
+ executeEffectsOnOutputChange(fiber, changedOutputKeys);
1076
+ }
1077
+ if (process.env.CREACT_DEBUG === 'true') {
1078
+ logger.debug(`Executed reactive effects for ${affectedFibers.length} fibers`);
1079
+ }
1080
+ }
1081
+ catch (error) {
1082
+ logger.error('Error executing reactive effects:', error);
1083
+ }
1084
+ }
1085
+ /**
1086
+ * Integrate with post-deployment effects to trigger output synchronization
1087
+ * This method should be called after deployment completes
1088
+ *
1089
+ * Phase ordering (REQ-1.1, 1.2, 1.5):
1090
+ * 1. Sync outputs to original nodes BEFORE executing effects
1091
+ * 2. Execute effects (they can now see updated outputs)
1092
+ * 3. Sync outputs and trigger re-renders for reactive changes
1093
+ * 4. Sync state changes back to CloudDOM
1094
+ *
1095
+ * @param fiber - Root fiber node
1096
+ * @param cloudDOM - Current CloudDOM after deployment
1097
+ * @param previousCloudDOM - Previous CloudDOM state (optional)
1098
+ * @returns Promise resolving to affected fibers that need re-rendering
1099
+ */
1100
+ async integrateWithPostDeploymentEffects(fiber, cloudDOM, previousCloudDOM) {
1101
+ logger.info('*** Starting post-deployment integration ***');
1102
+ logger.debug(`CloudDOM nodes: ${cloudDOM.length}`);
1103
+ logger.debug(`Fiber path: ${fiber.path?.join('.')}`);
1104
+ try {
1105
+ // PHASE 1: Sync outputs to original nodes FIRST (REQ-1.1, 1.4)
1106
+ // This ensures effects have access to current provider outputs
1107
+ logger.debug('PHASE 1: Syncing outputs to original nodes...');
1108
+ try {
1109
+ this.syncCloudDOMOutputsToOriginalNodes(fiber, cloudDOM);
1110
+ logger.debug('✓ Phase 1 complete: Outputs synced to original nodes');
1111
+ }
1112
+ catch (error) {
1113
+ logger.error('✗ Phase 1 failed: Error syncing outputs to original nodes:', error);
1114
+ throw new Error(`Output sync failed: ${error.message}`);
1115
+ }
1116
+ // PHASE 2: Execute post-deployment effects (REQ-1.2, 1.5)
1117
+ // Effects now have access to current outputs from Phase 1
1118
+ logger.debug('PHASE 2: Executing post-deployment effects...');
1119
+ try {
1120
+ await this.executePostDeploymentEffects(fiber);
1121
+ logger.debug('✓ Phase 2 complete: Effects executed with current outputs');
1122
+ }
1123
+ catch (error) {
1124
+ logger.error('✗ Phase 2 failed: Error executing effects:', error);
1125
+ // Continue to next phase even if effects fail (non-critical)
1126
+ logger.warn('Continuing to next phase despite effect execution failure');
1127
+ }
1128
+ // PHASE 3: Sync outputs and trigger re-renders (REQ-1.5)
1129
+ // Detect output changes and update bound state
1130
+ logger.debug('PHASE 3: Syncing outputs and triggering re-renders...');
1131
+ let affectedFibers = [];
1132
+ try {
1133
+ affectedFibers = await this.syncOutputsAndReRender(fiber, cloudDOM, previousCloudDOM);
1134
+ logger.debug(`✓ Phase 3 complete: ${affectedFibers.length} fibers affected by output changes`);
1135
+ }
1136
+ catch (error) {
1137
+ logger.error('✗ Phase 3 failed: Error syncing outputs and re-renders:', error);
1138
+ // Continue to next phase even if re-render sync fails (non-critical)
1139
+ logger.warn('Continuing to next phase despite re-render sync failure');
1140
+ }
1141
+ // PHASE 4: Sync state changes back to CloudDOM
1142
+ // Persist any state changes from effects
1143
+ logger.debug('PHASE 4: Syncing fiber state to CloudDOM...');
1144
+ try {
1145
+ this.syncFiberStateToCloudDOM(fiber, cloudDOM);
1146
+ logger.debug('✓ Phase 4 complete: Fiber state synced to CloudDOM');
1147
+ }
1148
+ catch (error) {
1149
+ logger.error('✗ Phase 4 failed: Error syncing fiber state:', error);
1150
+ // Continue despite failure (state will be synced on next deployment)
1151
+ logger.warn('State sync failed but deployment can continue');
1152
+ }
1153
+ logger.info(`*** Post-deployment integration complete: ${affectedFibers.length} affected fibers ***`);
1154
+ return affectedFibers;
1155
+ }
1156
+ catch (error) {
1157
+ logger.error('✗✗✗ Critical error in post-deployment integration:', error);
1158
+ logger.error('Stack trace:', error.stack);
1159
+ // Return empty array to allow deployment to complete
1160
+ // The error has been logged for debugging
1161
+ return [];
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Clear the existing node map (for testing or fresh builds)
1166
+ * This allows starting with a clean slate for node validation
1167
+ */
1168
+ clearExistingNodes() {
1169
+ this.existingNodeMap.clear();
1170
+ }
1171
+ /**
1172
+ * Get the existing node map (for debugging/inspection)
1173
+ * Returns a copy to prevent external mutation
1174
+ */
1175
+ getExistingNodes() {
1176
+ return new Map(this.existingNodeMap);
1177
+ }
1178
+ /**
1179
+ * Create a JSON replacer function that handles Symbols safely
1180
+ * Converts Symbols to their string representation for debugging
1181
+ */
1182
+ createSymbolReplacer() {
1183
+ return (key, value) => {
1184
+ if (typeof value === 'symbol') {
1185
+ return String(value);
1186
+ }
1187
+ if (typeof value === 'function' && value._contextId && typeof value._contextId === 'symbol') {
1188
+ // Handle context provider/consumer functions with Symbol IDs
1189
+ return {
1190
+ ...value,
1191
+ _contextId: String(value._contextId),
1192
+ };
1193
+ }
1194
+ return value;
1195
+ };
1196
+ }
1197
+ }
1198
+ exports.CloudDOMBuilder = CloudDOMBuilder;