@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,1127 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.getCReactInstance = exports.CReact = void 0;
33
+ // REQ-01, REQ-04, REQ-05, REQ-07, REQ-09: CReact orchestrator - main class
34
+ // Orchestrates the entire pipeline: render → validate → build → deploy
35
+ const Renderer_1 = require("./Renderer");
36
+ const Validator_1 = require("./Validator");
37
+ const CloudDOMBuilder_1 = require("./CloudDOMBuilder");
38
+ const StateMachine_1 = require("./StateMachine");
39
+ const Reconciler_1 = require("./Reconciler");
40
+ const types_1 = require("./types");
41
+ const EventBus_1 = require("./EventBus");
42
+ const useInstance_1 = require("../hooks/useInstance");
43
+ const useState_1 = require("../hooks/useState");
44
+ const useContext_1 = require("../hooks/useContext");
45
+ const errors_1 = require("./errors");
46
+ const jsx_1 = require("../jsx");
47
+ const RenderScheduler_1 = require("./RenderScheduler");
48
+ const StateBindingManager_1 = require("./StateBindingManager");
49
+ const ProviderOutputTracker_1 = require("./ProviderOutputTracker");
50
+ const ContextDependencyTracker_1 = require("./ContextDependencyTracker");
51
+ const ErrorRecoveryManager_1 = require("./ErrorRecoveryManager");
52
+ const StructuralChangeDetector_1 = require("./StructuralChangeDetector");
53
+ const Logger_1 = require("../utils/Logger");
54
+ const logger = Logger_1.LoggerFactory.getLogger('runtime');
55
+ /**
56
+ * CReact orchestrator - main class
57
+ *
58
+ * Orchestrates the entire infrastructure-as-code pipeline:
59
+ * 1. Render JSX → Fiber tree
60
+ * 2. Validate Fiber tree
61
+ * 3. Build CloudDOM from Fiber
62
+ * 4. Compare CloudDOM trees (diff via Reconciler)
63
+ * 5. Deploy CloudDOM via StateMachine
64
+ *
65
+ * REQ-01: JSX → CloudDOM rendering
66
+ * REQ-04: Dependency injection pattern
67
+ * REQ-05: Deployment orchestration via StateMachine
68
+ * REQ-07: Validation before commit/deploy
69
+ * REQ-09: Provider lifecycle hooks
70
+ * REQ-O01: StateMachine handles all state management
71
+ */
72
+ class CReact {
73
+ /**
74
+ * Constructor receives all dependencies via config (dependency injection)
75
+ *
76
+ * REQ-04: Providers are injected, not inherited
77
+ *
78
+ * @param config - Configuration with injected providers
79
+ */
80
+ constructor(config) {
81
+ this.config = config;
82
+ this.lastFiberTree = null; // Store the last rendered Fiber tree for effects
83
+ // Reactive deployment tracking
84
+ this.hasReactiveChanges = false;
85
+ this.reactiveCloudDOM = null;
86
+ this.preReactiveCloudDOM = null; // CloudDOM before re-render
87
+ // Set global instance for hooks to access
88
+ CReact.globalInstance = this;
89
+ // Instantiate core components
90
+ this.renderer = new Renderer_1.Renderer();
91
+ this.validator = new Validator_1.Validator();
92
+ // Inject cloud provider into CloudDOMBuilder (REQ-04)
93
+ this.cloudDOMBuilder = new CloudDOMBuilder_1.CloudDOMBuilder(config.cloudProvider);
94
+ // Instantiate Reconciler for diff computation
95
+ this.reconciler = new Reconciler_1.Reconciler();
96
+ // Instantiate StateMachine for deployment orchestration (REQ-O01)
97
+ this.stateMachine = new StateMachine_1.StateMachine(config.backendProvider);
98
+ // Initialize reactive system components
99
+ this.renderScheduler = new RenderScheduler_1.RenderScheduler(config.eventHooks);
100
+ this.stateBindingManager = new StateBindingManager_1.StateBindingManager();
101
+ this.providerOutputTracker = new ProviderOutputTracker_1.ProviderOutputTracker(config.eventHooks);
102
+ this.contextDependencyTracker = new ContextDependencyTracker_1.ContextDependencyTracker(config.eventHooks);
103
+ this.errorRecoveryManager = new ErrorRecoveryManager_1.ErrorRecoveryManager(config.eventHooks);
104
+ this.structuralChangeDetector = new StructuralChangeDetector_1.StructuralChangeDetector(config.eventHooks);
105
+ // Wire up reactive components
106
+ this.renderer.setRenderScheduler(this.renderScheduler);
107
+ this.renderer.setContextDependencyTracker(this.contextDependencyTracker);
108
+ this.renderer.setStructuralChangeDetector(this.structuralChangeDetector);
109
+ this.cloudDOMBuilder.setReactiveComponents(this.stateBindingManager, this.providerOutputTracker);
110
+ this.contextDependencyTracker.setStateBindingManager(this.stateBindingManager);
111
+ // Set the context dependency tracker in useContext hook
112
+ (0, useContext_1.setContextDependencyTracker)(this.contextDependencyTracker);
113
+ // Set the state binding manager in useState hook
114
+ (0, useState_1.setStateBindingManager)(this.stateBindingManager);
115
+ // Set the provider output tracker in useInstance hook
116
+ (0, useInstance_1.setProviderOutputTracker)(this.providerOutputTracker);
117
+ // Subscribe to provider output change events (event-driven reactivity)
118
+ if (this.config.cloudProvider.on) {
119
+ this.config.cloudProvider.on('outputsChanged', (change) => {
120
+ this.handleProviderOutputChange(change).catch((error) => {
121
+ logger.error('Error handling provider output change:', error);
122
+ });
123
+ });
124
+ logger.debug('Subscribed to provider output change events');
125
+ }
126
+ }
127
+ /**
128
+ * Debug logging helper
129
+ * Logs messages when CREACT_DEBUG environment variable is set
130
+ *
131
+ * @param message - Message to log
132
+ */
133
+ log(message) {
134
+ logger.debug(message);
135
+ }
136
+ /**
137
+ * Get the global CReact instance for hooks to access
138
+ * @internal
139
+ */
140
+ static getCReactInstance() {
141
+ return CReact.globalInstance;
142
+ }
143
+ /**
144
+ * Schedule a component for re-rendering
145
+ * This is called by hooks when reactive state changes
146
+ *
147
+ * @param fiber - Fiber node to re-render
148
+ * @param reason - Reason for the re-render
149
+ */
150
+ scheduleReRender(fiber, reason) {
151
+ this.log(`Scheduling re-render for ${fiber.path.join('.')} (reason: ${reason})`);
152
+ this.renderScheduler.schedule(fiber, reason);
153
+ }
154
+ /**
155
+ * Handle output change events from provider (event-driven reactivity)
156
+ * Called when provider emits 'outputsChanged' event
157
+ *
158
+ * This enables real-time reactivity without polling:
159
+ * 1. Update ProviderOutputTracker with new outputs
160
+ * 2. Execute useEffect callbacks bound to these outputs
161
+ * 3. Update bound state and enqueue affected fibers for re-render
162
+ *
163
+ * @param change - Output change event from provider
164
+ */
165
+ async handleProviderOutputChange(change) {
166
+ if (!this.lastFiberTree) {
167
+ return; // No fiber tree to update
168
+ }
169
+ logger.debug(`Provider output changed: ${change.nodeId}`, change.outputs);
170
+ // Step 1: Update ProviderOutputTracker
171
+ const outputChanges = this.providerOutputTracker.updateInstanceOutputs(change.nodeId, change.outputs);
172
+ if (outputChanges.length === 0) {
173
+ return; // No actual changes detected
174
+ }
175
+ // Step 2: Execute useEffect callbacks bound to these outputs
176
+ // TODO: Implement executeEffectsOnOutputChange when useEffect is ready
177
+ // await executeEffectsOnOutputChange(this.lastFiberTree, outputChanges);
178
+ // Step 3: Update bound state and get affected fibers
179
+ const affectedFibers = this.stateBindingManager.processOutputChanges(outputChanges);
180
+ if (affectedFibers.length > 0) {
181
+ logger.debug(`Output change affected ${affectedFibers.length} fibers`);
182
+ // Schedule re-renders for affected components
183
+ affectedFibers.forEach((fiber) => {
184
+ this.scheduleReRender(fiber, 'output-update');
185
+ });
186
+ // Note: Actual re-render execution happens in the next deployment cycle
187
+ // or can be triggered immediately via renderScheduler.flushBatch()
188
+ }
189
+ }
190
+ /**
191
+ * Handle context value changes and trigger selective re-renders
192
+ * This is called when a context provider value changes
193
+ *
194
+ * @param contextId - Context identifier
195
+ * @param newValue - New context value
196
+ * @returns Promise resolving to affected fibers that were re-rendered
197
+ */
198
+ async handleContextChange(contextId, newValue) {
199
+ this.log(`Context change detected for context: ${String(contextId)}`);
200
+ try {
201
+ // Update context value and get affected fibers
202
+ const affectedFibers = this.contextDependencyTracker.updateContextValue(contextId, newValue);
203
+ if (affectedFibers.length === 0) {
204
+ this.log('No components affected by context change');
205
+ return [];
206
+ }
207
+ this.log(`Context change affects ${affectedFibers.length} components`);
208
+ // Schedule re-renders for affected components
209
+ affectedFibers.forEach((fiber) => {
210
+ this.scheduleReRender(fiber, 'context-change');
211
+ });
212
+ // Execute the scheduled re-renders
213
+ const updatedFiber = this.renderer.reRenderComponents(affectedFibers, 'context-change');
214
+ // Update the last fiber tree
215
+ this.lastFiberTree = updatedFiber;
216
+ return affectedFibers;
217
+ }
218
+ catch (error) {
219
+ this.log(`Context change handling failed: ${error.message}`);
220
+ // Attempt rollback
221
+ const rollbackSuccess = this.contextDependencyTracker.rollbackContextValue(contextId);
222
+ if (rollbackSuccess) {
223
+ this.log('Context value rolled back successfully');
224
+ }
225
+ throw error;
226
+ }
227
+ }
228
+ /**
229
+ * Manual re-render trigger for CLI/testing
230
+ * Re-renders the entire stack or specific components
231
+ *
232
+ * @param stackName - Stack name to re-render (default: 'default')
233
+ * @param targetComponents - Optional specific components to re-render
234
+ * @returns Promise resolving to updated CloudDOM
235
+ */
236
+ async rerender(stackName = 'default', targetComponents) {
237
+ this.log(`Manual re-render triggered for stack: ${stackName}`);
238
+ try {
239
+ // Get current state
240
+ const currentState = await this.stateMachine.getState(stackName);
241
+ if (!currentState?.cloudDOM) {
242
+ throw new Error(`No existing state found for stack: ${stackName}`);
243
+ }
244
+ // If no target components specified, re-render from last fiber tree
245
+ if (!targetComponents && this.lastFiberTree) {
246
+ this.log('Re-rendering entire stack from last fiber tree');
247
+ // Re-render the entire fiber tree
248
+ const updatedFiber = this.renderer.reRenderComponents([this.lastFiberTree], 'manual');
249
+ this.lastFiberTree = updatedFiber;
250
+ // Build updated CloudDOM
251
+ const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
252
+ // Sync outputs and trigger any additional re-renders
253
+ await this.cloudDOMBuilder.syncOutputsAndReRender(updatedFiber, updatedCloudDOM, currentState.cloudDOM);
254
+ return updatedCloudDOM;
255
+ }
256
+ // Re-render specific components
257
+ if (targetComponents && targetComponents.length > 0) {
258
+ this.log(`Re-rendering ${targetComponents.length} specific components`);
259
+ const updatedFiber = this.renderer.reRenderComponents(targetComponents, 'manual');
260
+ const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
261
+ return updatedCloudDOM;
262
+ }
263
+ throw new Error('No components available for re-rendering');
264
+ }
265
+ catch (error) {
266
+ this.log(`Re-render failed: ${error.message}`);
267
+ throw error;
268
+ }
269
+ }
270
+ /**
271
+ * Build CloudDOM from JSX
272
+ *
273
+ * Pipeline: render → validate → build → restore outputs
274
+ *
275
+ * REQ-01: JSX → CloudDOM rendering
276
+ * REQ-07: Validate before commit
277
+ *
278
+ * @param jsx - JSX element to render
279
+ * @param stackName - Stack name for state restoration (default: 'default')
280
+ * @returns Promise resolving to CloudDOM tree
281
+ */
282
+ async build(jsx, stackName = 'default') {
283
+ this.log('Starting build pipeline');
284
+ // Step 1: Load previous state to get outputs (via StateMachine)
285
+ this.log('Loading previous state');
286
+ const previousState = await this.stateMachine.getState(stackName);
287
+ // Step 2: If we have previous state, prepare hydration for useState AND inject outputs for useInstance
288
+ if (previousState?.cloudDOM) {
289
+ // CRITICAL: Prepare hydration BEFORE rendering so useState can restore values
290
+ this.log('Preparing hydration for useState');
291
+ this.prepareHydration(previousState.cloudDOM);
292
+ this.log('Injecting previous outputs into useInstance hook');
293
+ (0, useInstance_1.setPreviousOutputs)(this.buildOutputsMap(previousState.cloudDOM));
294
+ }
295
+ // Step 3: Render JSX → Fiber (with hydration and outputs available)
296
+ this.log('Rendering JSX to Fiber tree');
297
+ const fiber = this.renderer.render(jsx);
298
+ // Step 4: Store the Fiber tree for post-deployment effects
299
+ this.lastFiberTree = fiber;
300
+ // Step 5: Clear hydration and previous outputs after render
301
+ this.log('Clearing hydration data');
302
+ this.clearHydration();
303
+ (0, useInstance_1.setPreviousOutputs)(null);
304
+ // Step 6: Validate Fiber (REQ-07)
305
+ this.log('Validating Fiber tree');
306
+ this.validator.validate(fiber);
307
+ // Step 7: Build CloudDOM from Fiber (commit phase)
308
+ this.log('Building CloudDOM from Fiber');
309
+ const cloudDOM = await this.cloudDOMBuilder.build(fiber);
310
+ // Step 8: Detect structural changes if we have previous state
311
+ if (previousState?.cloudDOM) {
312
+ this.log('Detecting structural changes');
313
+ const structuralChanges = this.structuralChangeDetector.detectStructuralChanges(previousState.cloudDOM, cloudDOM, fiber);
314
+ if (structuralChanges.length > 0) {
315
+ this.log(`Detected ${structuralChanges.length} structural changes`);
316
+ // Trigger re-renders for affected components
317
+ this.structuralChangeDetector.triggerStructuralReRenders(structuralChanges, this.renderScheduler);
318
+ // Check if deployment plan needs updating
319
+ if (this.structuralChangeDetector.requiresDeploymentPlanUpdate(structuralChanges)) {
320
+ this.log('Structural changes require deployment plan update');
321
+ }
322
+ }
323
+ }
324
+ this.log('Build pipeline complete');
325
+ return cloudDOM;
326
+ }
327
+ /**
328
+ * Build a map of node ID → outputs from previous CloudDOM
329
+ *
330
+ * @param previousCloudDOM - Previous CloudDOM state
331
+ * @returns Map of node ID → outputs
332
+ */
333
+ buildOutputsMap(previousCloudDOM) {
334
+ const outputsMap = new Map();
335
+ const walk = (nodes) => {
336
+ for (const node of nodes) {
337
+ if (node.outputs && Object.keys(node.outputs).length > 0) {
338
+ outputsMap.set(node.id, node.outputs);
339
+ }
340
+ if (node.children && node.children.length > 0) {
341
+ walk(node.children);
342
+ }
343
+ }
344
+ };
345
+ walk(previousCloudDOM);
346
+ return outputsMap;
347
+ }
348
+ /**
349
+ * Build CloudDOM with error handling for CLI/CI environments
350
+ *
351
+ * Provides a safer entrypoint that handles errors gracefully without
352
+ * crashing the entire process. Useful for CI/CD pipelines.
353
+ *
354
+ * @param jsx - JSX element to render
355
+ * @returns Promise resolving to CloudDOM tree, or empty array on error
356
+ */
357
+ async buildSafe(jsx) {
358
+ try {
359
+ return await this.build(jsx);
360
+ }
361
+ catch (error) {
362
+ logger.error('Build failed:', error);
363
+ return [];
364
+ }
365
+ }
366
+ /**
367
+ * Compare two CloudDOM trees and return diff
368
+ *
369
+ * REQ-05: Reconciliation and diff
370
+ * REQ-07.6: Validate before comparing
371
+ *
372
+ * @param previous - Previous CloudDOM tree
373
+ * @param current - Current CloudDOM tree
374
+ * @returns ChangeSet with creates, updates, deletes, and deployment order
375
+ */
376
+ async compare(previous, current) {
377
+ // REQ-07.6: Validate before comparing
378
+ const currentFiber = this.renderer.getCurrentFiber();
379
+ if (currentFiber) {
380
+ this.validator.validate(currentFiber);
381
+ }
382
+ // Use Reconciler to compute diff
383
+ this.log('Computing diff between previous and current CloudDOM');
384
+ return this.reconciler.reconcile(previous, current);
385
+ }
386
+ /**
387
+ * Get reactive deployment information if changes are pending
388
+ * Returns null if no reactive changes detected
389
+ *
390
+ * This encapsulates the logic for checking reactive changes and computing diffs,
391
+ * keeping separation of concerns between core and CLI layers.
392
+ *
393
+ * @param stackName - Stack name to compute diff against
394
+ * @returns Reactive deployment info with CloudDOM and ChangeSet, or null
395
+ */
396
+ async getReactiveDeploymentInfo(stackName) {
397
+ if (!this.hasReactiveChanges || !this.reactiveCloudDOM || !this.preReactiveCloudDOM) {
398
+ return null;
399
+ }
400
+ // Compute diff between pre-reactive and post-reactive CloudDOM
401
+ // This shows what NEW resources were created by the re-render
402
+ const changeSet = this.reconciler.reconcile(this.preReactiveCloudDOM, this.reactiveCloudDOM);
403
+ return {
404
+ cloudDOM: this.reactiveCloudDOM,
405
+ changeSet,
406
+ };
407
+ }
408
+ /**
409
+ * Clear reactive changes flag (called after deployment)
410
+ */
411
+ clearReactiveChanges() {
412
+ this.hasReactiveChanges = false;
413
+ this.reactiveCloudDOM = null;
414
+ this.preReactiveCloudDOM = null;
415
+ }
416
+ /**
417
+ * Deploy CloudDOM to cloud provider using StateMachine
418
+ *
419
+ * Pipeline: validate → compute diff → start deployment → materialize → checkpoint → complete
420
+ *
421
+ * REQ-05: Deployment orchestration via StateMachine
422
+ * REQ-05.4: Idempotent deployment (via Reconciler diff)
423
+ * REQ-07.6: Validate before deploying
424
+ * REQ-09: Provider lifecycle hooks
425
+ * REQ-O01: StateMachine handles all state management
426
+ *
427
+ * @param cloudDOM - CloudDOM tree to deploy
428
+ * @param stackName - Stack name for state management (default: 'default')
429
+ * @param user - User initiating deployment (default: 'system')
430
+ */
431
+ async deploy(cloudDOM, stackName = 'default', user = 'system') {
432
+ // Clear reactive changes flag at start of deployment
433
+ this.clearReactiveChanges();
434
+ // REQ-07.6: Validate before deploying
435
+ const currentFiber = this.renderer.getCurrentFiber();
436
+ if (currentFiber) {
437
+ this.validator.validate(currentFiber);
438
+ }
439
+ // REQ-05.4: Compute diff for idempotent deployment
440
+ this.log('Computing diff for idempotent deployment');
441
+ const previousState = await this.stateMachine.getState(stackName);
442
+ const previousCloudDOM = previousState?.cloudDOM || [];
443
+ const changeSet = this.reconciler.reconcile(previousCloudDOM, cloudDOM);
444
+ // Use single source of truth for checking changes
445
+ if (!(0, Reconciler_1.hasChanges)(changeSet)) {
446
+ this.log('No changes detected. Deployment skipped.');
447
+ this.log('No resources to deploy');
448
+ return;
449
+ }
450
+ this.log(`Changes detected: ${changeSet.creates.length} creates, ${changeSet.updates.length} updates, ${changeSet.deletes.length} deletes`);
451
+ this.log(`Deployment order: ${changeSet.deploymentOrder.length} resources`);
452
+ try {
453
+ // Start deployment via StateMachine
454
+ this.log('Starting deployment via StateMachine');
455
+ await this.stateMachine.startDeployment(stackName, changeSet, cloudDOM, user);
456
+ // REQ-09.1: Lifecycle hook - preDeploy
457
+ if (this.config.cloudProvider.preDeploy) {
458
+ this.log('Calling preDeploy lifecycle hook');
459
+ await this.config.cloudProvider.preDeploy(cloudDOM);
460
+ }
461
+ // Process deletes first (before creates/updates to avoid conflicts)
462
+ if (changeSet.deletes.length > 0) {
463
+ this.log(`Processing ${changeSet.deletes.length} deletes`);
464
+ for (const deleteNode of changeSet.deletes) {
465
+ this.log(`Deleting resource: ${deleteNode.id}`);
466
+ // Trigger onDestroy event callback for this resource
467
+ await EventBus_1.CloudDOMEventBus.triggerEventCallbacks(deleteNode, 'destroy');
468
+ // Note: Actual deletion would be handled by the cloud provider
469
+ // For now, we just trigger the callback
470
+ // TODO: Add actual deletion logic when provider supports it
471
+ }
472
+ }
473
+ // Deploy resources in order with checkpoints
474
+ // Only deploy resources that have changes (creates, updates, replacements)
475
+ const filteredDeploymentOrder = (0, types_1.getResourcesToDeploy)(changeSet);
476
+ this.log('Deploying resources with checkpoints');
477
+ this.log(`Total resources: ${changeSet.deploymentOrder.length}, Resources to deploy: ${filteredDeploymentOrder.length}`);
478
+ for (let i = 0; i < filteredDeploymentOrder.length; i++) {
479
+ const resourceId = filteredDeploymentOrder[i];
480
+ this.log(`Deploying resource ${i + 1}/${filteredDeploymentOrder.length}: ${resourceId}`);
481
+ // Find the resource node
482
+ const resourceNode = this.findNodeById(cloudDOM, resourceId);
483
+ if (!resourceNode) {
484
+ throw new errors_1.DeploymentError(`Resource not found: ${resourceId}`, {
485
+ message: `Resource not found: ${resourceId}`,
486
+ code: 'RESOURCE_NOT_FOUND',
487
+ details: { resourceId, stackName },
488
+ });
489
+ }
490
+ // Materialize single resource
491
+ await this.config.cloudProvider.materialize([resourceNode], null);
492
+ // Trigger onDeploy event callback for this resource
493
+ await EventBus_1.CloudDOMEventBus.triggerEventCallbacks(resourceNode, 'deploy');
494
+ // Update checkpoint after successful deployment
495
+ await this.stateMachine.updateCheckpoint(stackName, i);
496
+ }
497
+ // Collect outputs from materialization (REQ-02, REQ-06)
498
+ this.log('Extracting outputs from CloudDOM');
499
+ const outputs = this.extractOutputs(cloudDOM);
500
+ // REQ-09.2: Lifecycle hook - postDeploy
501
+ if (this.config.cloudProvider.postDeploy) {
502
+ this.log('Calling postDeploy lifecycle hook');
503
+ await this.config.cloudProvider.postDeploy(cloudDOM, outputs);
504
+ }
505
+ // Complete deployment via StateMachine
506
+ this.log('Completing deployment via StateMachine');
507
+ await this.stateMachine.completeDeployment(stackName);
508
+ // Note: Previously marked initial build as complete, but now reactive system
509
+ // works throughout the entire lifecycle thanks to improved CloudDOMBuilder
510
+ // Execute post-deployment effects with reactive output synchronization
511
+ this.log('Executing post-deployment effects with reactive sync');
512
+ if (this.lastFiberTree) {
513
+ this.log('Executing post-deployment effects...');
514
+ // Integrate with post-deployment effects and output sync
515
+ const affectedFibers = await this.cloudDOMBuilder.integrateWithPostDeploymentEffects(this.lastFiberTree, cloudDOM, previousCloudDOM);
516
+ // REQ-7.1, 7.2, 7.3: Check if outputs actually changed (including undefined → value)
517
+ const hasActualChanges = this.hasActualOutputChanges(previousCloudDOM, cloudDOM);
518
+ // CRITICAL: If state outputs changed but no provider output bindings triggered,
519
+ // we still need to re-render to display updated state in the component
520
+ const needsReRender = hasActualChanges || affectedFibers.length > 0;
521
+ if (needsReRender) {
522
+ // If no fibers were affected by provider outputs, but state changed,
523
+ // re-render the root component to display updated state
524
+ const fibersToReRender = affectedFibers.length > 0 ? affectedFibers : [this.lastFiberTree];
525
+ this.log(`Triggering re-renders for ${fibersToReRender.length} affected components (${hasActualChanges ? 'output changes detected' : 'state changes detected'})`);
526
+ // REQ-7.4, 7.5: Schedule re-renders with proper batching and deduplication
527
+ fibersToReRender.forEach((fiber) => {
528
+ this.scheduleReRender(fiber, 'output-update');
529
+ });
530
+ // CRITICAL: Update previousOutputsMap with current CloudDOM outputs
531
+ // This allows useInstance to access outputs during re-render
532
+ const outputsMap = this.buildOutputsMap(cloudDOM);
533
+ logger.debug('Updating previousOutputsMap for re-render with latest outputs');
534
+ logger.debug(`OutputsMap has ${outputsMap.size} entries:`, Array.from(outputsMap.keys()));
535
+ (0, useInstance_1.setPreviousOutputs)(outputsMap);
536
+ // Execute the scheduled re-renders
537
+ const updatedFiber = this.renderer.reRenderComponents(fibersToReRender, 'output-update');
538
+ // Build updated CloudDOM from re-rendered components
539
+ const updatedCloudDOM = await this.cloudDOMBuilder.build(updatedFiber);
540
+ // Check if the re-render produced new resources to deploy
541
+ const reactiveChangeSet = this.reconciler.reconcile(cloudDOM, updatedCloudDOM);
542
+ if ((0, Reconciler_1.hasChanges)(reactiveChangeSet)) {
543
+ this.log(`Re-render produced new changes: ${reactiveChangeSet.creates.length} creates, ${reactiveChangeSet.updates.length} updates`);
544
+ this.log('Reactive changes detected - will need another deployment cycle');
545
+ // Store the CloudDOM BEFORE re-render for diff comparison
546
+ this.preReactiveCloudDOM = JSON.parse(JSON.stringify(cloudDOM));
547
+ // Update the CloudDOM with reactive changes
548
+ cloudDOM.splice(0, cloudDOM.length, ...updatedCloudDOM);
549
+ // Store flag indicating reactive changes need deployment
550
+ this.hasReactiveChanges = true;
551
+ this.reactiveCloudDOM = updatedCloudDOM;
552
+ this.log('Post-deployment effects and reactive sync completed');
553
+ this.log('Deployment complete (reactive changes pending)');
554
+ return;
555
+ }
556
+ else {
557
+ this.log('Re-render produced no new changes');
558
+ // Update the stored CloudDOM with reactive changes
559
+ cloudDOM.splice(0, cloudDOM.length, ...updatedCloudDOM);
560
+ }
561
+ }
562
+ else {
563
+ logger.debug(`No output or state changes detected, skipping re-render`);
564
+ }
565
+ // Save the updated CloudDOM state with new outputs
566
+ logger.debug('Saving updated state with new outputs...');
567
+ await this.stateMachine.updateCloudDOM(stackName, cloudDOM);
568
+ this.log('Post-deployment effects and reactive sync completed');
569
+ }
570
+ else {
571
+ logger.warn('No fiber tree found for effects execution');
572
+ }
573
+ this.log('Deployment complete');
574
+ }
575
+ catch (error) {
576
+ logger.error('Deployment failed:', error);
577
+ // NEW (Gap 3): Attempt error recovery before failing
578
+ if (this.lastFiberTree) {
579
+ try {
580
+ logger.info('Attempting error recovery...');
581
+ // Create snapshot for potential rollback
582
+ this.errorRecoveryManager.createComponentSnapshot(this.lastFiberTree);
583
+ // Attempt recovery using re-render error handler
584
+ const recoveryResult = await this.errorRecoveryManager.handleReRenderError(error, [this.lastFiberTree], 'manual');
585
+ if (recoveryResult.success) {
586
+ logger.info(`Error recovery succeeded: ${recoveryResult.message}`);
587
+ // Recovery succeeded - return partial result instead of throwing
588
+ // This allows deployment to complete with recovered state
589
+ return;
590
+ }
591
+ logger.warn(`Error recovery failed: ${recoveryResult.message}`);
592
+ }
593
+ catch (recoveryError) {
594
+ logger.error('Error during recovery attempt:', recoveryError);
595
+ // Continue with normal error handling
596
+ }
597
+ }
598
+ // Trigger onError event callbacks for all resources
599
+ await EventBus_1.CloudDOMEventBus.triggerEventCallbacksRecursive(cloudDOM, 'error', error);
600
+ // REQ-09.3: Lifecycle hook - onError
601
+ if (this.config.cloudProvider.onError) {
602
+ this.log('Calling onError lifecycle hook');
603
+ await this.config.cloudProvider.onError(error, cloudDOM);
604
+ }
605
+ // Mark deployment as failed via StateMachine
606
+ this.log('Marking deployment as failed via StateMachine');
607
+ const deploymentError = error instanceof errors_1.DeploymentError
608
+ ? error
609
+ : new errors_1.DeploymentError(error.message, {
610
+ message: error.message,
611
+ code: 'DEPLOYMENT_FAILED',
612
+ stack: error.stack,
613
+ });
614
+ await this.stateMachine.failDeployment(stackName, deploymentError);
615
+ // Re-throw error to halt deployment (REQ-09.4)
616
+ throw error;
617
+ }
618
+ }
619
+ // Hydration map is now STATIC (see declaration above with globalInstance)
620
+ /**
621
+ * Load previous state from backend and prepare for hydration
622
+ * This should be called before rendering to restore persisted state
623
+ *
624
+ * @param stackName - Stack name to load state for
625
+ */
626
+ async loadStateForHydration(stackName) {
627
+ try {
628
+ const previousState = await this.stateMachine.getState(stackName);
629
+ if (previousState && previousState.cloudDOM) {
630
+ this.prepareHydration(previousState.cloudDOM);
631
+ this.log(`Loaded state for hydration from backend: ${stackName}`);
632
+ }
633
+ else {
634
+ this.log(`No previous state found for stack: ${stackName}`);
635
+ }
636
+ }
637
+ catch (error) {
638
+ logger.warn(`Failed to load state for hydration: ${error.message}`);
639
+ // Continue without hydration - this is not a fatal error
640
+ }
641
+ }
642
+ /**
643
+ * Prepare hydration data from previous CloudDOM
644
+ * This must be called BEFORE rendering to make state available to useState
645
+ *
646
+ * CRITICAL: State outputs belong to the COMPONENT that called useState,
647
+ * not the CloudDOM node. We need to key by component path (parent of node).
648
+ *
649
+ * Example:
650
+ * - Component path: ['web-app-stack']
651
+ * - CloudDOM node paths: ['web-app-stack', 'vpc'], ['web-app-stack', 'database']
652
+ * - All nodes from same component share the same state outputs
653
+ * - Hydration should be keyed by 'web-app-stack', not 'web-app-stack.vpc'
654
+ *
655
+ * @param previousCloudDOM - Previous CloudDOM with persisted state outputs
656
+ * @param clearExisting - Whether to clear existing hydration data (default: false)
657
+ */
658
+ prepareHydration(previousCloudDOM, clearExisting = false) {
659
+ if (clearExisting) {
660
+ CReact.hydrationMap.clear();
661
+ }
662
+ if (!previousCloudDOM || previousCloudDOM.length === 0) {
663
+ this.log('No previous state to prepare for hydration');
664
+ return;
665
+ }
666
+ // Extract state outputs from previous CloudDOM and build hydration map
667
+ const extractStateOutputs = (nodes) => {
668
+ for (const node of nodes) {
669
+ // CRITICAL FIX: Extract component path (parent of CloudDOM node)
670
+ // node.path = ['web-app-stack', 'vpc']
671
+ // componentPath = 'web-app-stack' (where useState was called)
672
+ const componentPath = node.path.length > 1
673
+ ? node.path.slice(0, -1).join('.') // Normal case: parent component
674
+ : node.path.join('.'); // Edge case: root node
675
+ // Validate path
676
+ if (!componentPath) {
677
+ logger.warn(`Invalid component path for hydration:`, node.path);
678
+ continue;
679
+ }
680
+ // Read from state field instead of filtering state.* from outputs
681
+ if (node.state && Object.keys(node.state).length > 0) {
682
+ // Convert state object to array: { state1: "a", state2: "b" } → ["a", "b"]
683
+ // Ensure keys are sorted (state1, state2, state3...) before converting to array
684
+ const stateValues = Object.keys(node.state)
685
+ .sort() // Sort to ensure state1, state2, state3... order
686
+ .map((key) => node.state[key]);
687
+ if (stateValues.length > 0) {
688
+ // Only set if not already set (first node from component wins)
689
+ if (!CReact.hydrationMap.has(componentPath)) {
690
+ CReact.hydrationMap.set(componentPath, stateValues);
691
+ logger.debug(`Prepared hydration for component "${componentPath}" (from node "${node.path.join('.')}"): ${JSON.stringify(stateValues)}`);
692
+ }
693
+ else {
694
+ logger.debug(`Skipping duplicate hydration for component "${componentPath}" (already set from another node)`);
695
+ }
696
+ }
697
+ }
698
+ if (node.children && node.children.length > 0) {
699
+ extractStateOutputs(node.children);
700
+ }
701
+ }
702
+ };
703
+ extractStateOutputs(previousCloudDOM);
704
+ logger.debug(`Hydration prepared: ${CReact.hydrationMap.size} components with state`);
705
+ logger.debug(`Hydration map keys:`, Array.from(CReact.hydrationMap.keys()));
706
+ logger.debug(`This instance is global: ${this === CReact.globalInstance}`);
707
+ logger.debug(`Full hydration map:`, Object.fromEntries(CReact.hydrationMap));
708
+ }
709
+ /**
710
+ * Get hydrated value for a specific fiber and hook index
711
+ * Called by useState during render to check for persisted state
712
+ *
713
+ * @param fiberPath - Path of the fiber
714
+ * @param hookIndex - Index of the hook
715
+ * @returns Hydrated value or undefined if not found
716
+ */
717
+ getHydratedValue(fiberPath, hookIndex) {
718
+ const values = CReact.hydrationMap.get(fiberPath);
719
+ if (values && hookIndex < values.length) {
720
+ return values[hookIndex];
721
+ }
722
+ return undefined;
723
+ }
724
+ /**
725
+ * Get hydrated value for a component by searching for any child node
726
+ * This handles the case where useState is in a parent component but state
727
+ * outputs are stored on child CloudDOM nodes
728
+ *
729
+ * With the path mapping fix, this should now find exact matches.
730
+ * The child node search is kept as a fallback for edge cases.
731
+ *
732
+ * @param componentPath - Path of the component (e.g., 'web-app-stack')
733
+ * @param hookIndex - Index of the hook
734
+ * @returns Hydrated value or undefined if not found
735
+ */
736
+ getHydratedValueForComponent(componentPath, hookIndex) {
737
+ logger.debug(`getHydratedValueForComponent: path="${componentPath}", hookIndex=${hookIndex}`);
738
+ logger.debug(`Available hydration keys:`, Array.from(CReact.hydrationMap.keys()));
739
+ // Try exact match first (should work now with path mapping fix)
740
+ const exactMatch = this.getHydratedValue(componentPath, hookIndex);
741
+ if (exactMatch !== undefined) {
742
+ logger.debug(`✅ Found exact match for "${componentPath}"[${hookIndex}]:`, exactMatch);
743
+ return exactMatch;
744
+ }
745
+ // Fallback: Search for any child node that starts with this component path
746
+ // This handles edge cases where path mapping might not work as expected
747
+ for (const [nodePath, values] of CReact.hydrationMap.entries()) {
748
+ if (nodePath.startsWith(componentPath + '.') && hookIndex < values.length) {
749
+ logger.debug(`✅ Found hydration in child node: ${nodePath} for component: ${componentPath}`);
750
+ return values[hookIndex];
751
+ }
752
+ }
753
+ // No match found
754
+ logger.debug(`❌ No hydration found for "${componentPath}"[${hookIndex}]`);
755
+ return undefined;
756
+ }
757
+ /**
758
+ * Check if hydration data is available
759
+ *
760
+ * @returns True if hydration map has data
761
+ */
762
+ hasHydrationData() {
763
+ const hasData = CReact.hydrationMap.size > 0;
764
+ logger.debug(`hasHydrationData: size=${CReact.hydrationMap.size}, hasData=${hasData}, instance=${this === CReact.globalInstance ? 'GLOBAL' : 'OTHER'}`);
765
+ return hasData;
766
+ }
767
+ /**
768
+ * Get hydration map keys for debugging
769
+ *
770
+ * @returns Array of component paths with hydration data
771
+ */
772
+ getHydrationMapKeys() {
773
+ return Array.from(CReact.hydrationMap.keys());
774
+ }
775
+ /**
776
+ * Clear hydration data after rendering is complete
777
+ */
778
+ clearHydration() {
779
+ logger.debug(`clearHydration: Clearing ${CReact.hydrationMap.size} entries`);
780
+ logger.debug('clearHydration: Stack trace:', new Error().stack);
781
+ CReact.hydrationMap.clear();
782
+ this.log('Hydration data cleared');
783
+ }
784
+ /**
785
+ * Serialize internal reactive state for hot reload preservation
786
+ * This captures the state of reactive systems that need to survive recompilation
787
+ *
788
+ * @returns Serialized state object
789
+ */
790
+ serializeReactiveState() {
791
+ try {
792
+ const state = {
793
+ timestamp: Date.now(),
794
+ };
795
+ // Note: Most reactive state is already persisted in CloudDOM outputs
796
+ // This is mainly for tracking metadata and failure statistics
797
+ if (this.renderScheduler) {
798
+ // Preserve failure statistics but not pending renders
799
+ state.renderSchedulerStats = {
800
+ // Add any failure stats if the scheduler tracks them
801
+ };
802
+ }
803
+ return state;
804
+ }
805
+ catch (error) {
806
+ logger.warn('Failed to serialize reactive state:', error);
807
+ return null;
808
+ }
809
+ }
810
+ /**
811
+ * Restore internal reactive state after hot reload recompilation
812
+ * This restores the state of reactive systems from serialized data
813
+ *
814
+ * @param serializedState - Previously serialized state
815
+ */
816
+ restoreReactiveState(serializedState) {
817
+ if (!serializedState) {
818
+ this.log('No serialized state to restore');
819
+ return;
820
+ }
821
+ try {
822
+ // Clear any pending renders from the previous session
823
+ if (this.renderScheduler && this.renderScheduler.clearPending) {
824
+ this.renderScheduler.clearPending();
825
+ }
826
+ this.log('Reactive state restored from hot reload');
827
+ }
828
+ catch (error) {
829
+ logger.warn('Failed to restore reactive state:', error);
830
+ }
831
+ }
832
+ /**
833
+ * Hydrate the current fiber tree with state values from previous CloudDOM
834
+ * This is used during hot reload to restore persisted state values
835
+ *
836
+ * @param previousCloudDOM - Previous CloudDOM with persisted state outputs
837
+ */
838
+ hydrateStateFromPreviousCloudDOM(previousCloudDOM) {
839
+ if (!this.lastFiberTree || !previousCloudDOM || previousCloudDOM.length === 0) {
840
+ this.log('No previous state to hydrate');
841
+ return;
842
+ }
843
+ // Extract state outputs from previous CloudDOM
844
+ const stateOutputs = new Map();
845
+ const extractStateOutputs = (nodes) => {
846
+ for (const node of nodes) {
847
+ // Read from state field instead of filtering state.* from outputs
848
+ if (node.state && Object.keys(node.state).length > 0) {
849
+ // Convert state object to array: { state1: "a", state2: "b" } → ["a", "b"]
850
+ // Ensure keys are sorted (state1, state2, state3...) before converting to array
851
+ const stateValues = Object.keys(node.state)
852
+ .sort() // Sort to ensure state1, state2, state3... order
853
+ .map((key) => node.state[key]);
854
+ if (stateValues.length > 0) {
855
+ stateOutputs.set(node.id, stateValues);
856
+ }
857
+ }
858
+ if (node.children && node.children.length > 0) {
859
+ extractStateOutputs(node.children);
860
+ }
861
+ }
862
+ };
863
+ extractStateOutputs(previousCloudDOM);
864
+ if (stateOutputs.size === 0) {
865
+ this.log('No state outputs found in previous CloudDOM');
866
+ return;
867
+ }
868
+ // Hydrate fiber tree with state values
869
+ const hydrateFiber = (fiber) => {
870
+ // Match fiber to CloudDOM node by path
871
+ if (fiber.cloudDOMNodes && Array.isArray(fiber.cloudDOMNodes)) {
872
+ for (const node of fiber.cloudDOMNodes) {
873
+ const savedState = stateOutputs.get(node.id);
874
+ if (savedState && savedState.length > 0) {
875
+ // Initialize hooks array if not present
876
+ if (!fiber.hooks) {
877
+ fiber.hooks = [];
878
+ }
879
+ // Restore state values
880
+ for (let i = 0; i < savedState.length; i++) {
881
+ fiber.hooks[i] = savedState[i];
882
+ }
883
+ this.log(`Hydrated ${savedState.length} state values for ${node.id}`);
884
+ }
885
+ }
886
+ }
887
+ // Recursively hydrate children
888
+ if (fiber.children && fiber.children.length > 0) {
889
+ for (const child of fiber.children) {
890
+ hydrateFiber(child);
891
+ }
892
+ }
893
+ };
894
+ hydrateFiber(this.lastFiberTree);
895
+ this.log(`State hydration complete: ${stateOutputs.size} nodes hydrated`);
896
+ }
897
+ /**
898
+ * Check if outputs actually changed between previous and current CloudDOM
899
+ * REQ-7.1, 7.2, 7.3: Proper output change detection including undefined → value transitions
900
+ *
901
+ * @param previous - Previous CloudDOM state
902
+ * @param current - Current CloudDOM state
903
+ * @returns True if any outputs actually changed
904
+ */
905
+ hasActualOutputChanges(previous, current) {
906
+ // Build maps for efficient comparison
907
+ const previousMap = new Map();
908
+ const currentMap = new Map();
909
+ const buildMap = (nodes, map) => {
910
+ for (const node of nodes) {
911
+ if (node.outputs) {
912
+ map.set(node.id, node.outputs);
913
+ }
914
+ if (node.children && node.children.length > 0) {
915
+ buildMap(node.children, map);
916
+ }
917
+ }
918
+ };
919
+ buildMap(previous, previousMap);
920
+ buildMap(current, currentMap);
921
+ // Check for changed outputs in existing nodes
922
+ for (const [nodeId, currentOutputs] of currentMap) {
923
+ const previousOutputs = previousMap.get(nodeId) || {};
924
+ // Check each output key
925
+ for (const [outputKey, currentValue] of Object.entries(currentOutputs)) {
926
+ const previousValue = previousOutputs[outputKey];
927
+ // REQ-7.1, 7.3: undefined → value IS a change (initial deployment scenario)
928
+ if (previousValue !== currentValue) {
929
+ logger.debug(`Output changed: ${nodeId}.${outputKey} (${previousValue} → ${currentValue})`);
930
+ return true;
931
+ }
932
+ }
933
+ // Check for removed outputs
934
+ for (const outputKey of Object.keys(previousOutputs)) {
935
+ if (!(outputKey in currentOutputs)) {
936
+ logger.debug(`Output removed: ${nodeId}.${outputKey}`);
937
+ return true;
938
+ }
939
+ }
940
+ }
941
+ // Check for new nodes with outputs
942
+ for (const [nodeId, currentOutputs] of currentMap) {
943
+ if (!previousMap.has(nodeId) && Object.keys(currentOutputs).length > 0) {
944
+ logger.debug(`New node with outputs: ${nodeId}`);
945
+ return true;
946
+ }
947
+ }
948
+ return false;
949
+ }
950
+ /**
951
+ * Find a CloudDOM node by ID
952
+ *
953
+ * @param nodes - CloudDOM tree to search
954
+ * @param id - Node ID to find
955
+ * @returns CloudDOM node or undefined if not found
956
+ */
957
+ findNodeById(nodes, id) {
958
+ for (const node of nodes) {
959
+ if (node.id === id) {
960
+ return node;
961
+ }
962
+ if (node.children && node.children.length > 0) {
963
+ const found = this.findNodeById(node.children, id);
964
+ if (found) {
965
+ return found;
966
+ }
967
+ }
968
+ }
969
+ return undefined;
970
+ }
971
+ /**
972
+ * Extract outputs from CloudDOM nodes
973
+ *
974
+ * Walks the CloudDOM tree and collects all outputs from nodes.
975
+ * Outputs are formatted as nodeId.outputKey (e.g., 'registry.state-0').
976
+ *
977
+ * REQ-02: Extract outputs from useState calls
978
+ * REQ-06: Universal output access
979
+ *
980
+ * @param cloudDOM - CloudDOM tree
981
+ * @returns Outputs object with keys in format nodeId.outputKey
982
+ */
983
+ extractOutputs(cloudDOM) {
984
+ const outputs = {};
985
+ // Safety check
986
+ if (!Array.isArray(cloudDOM)) {
987
+ this.log('Warning: cloudDOM is not an array, returning empty outputs');
988
+ return outputs;
989
+ }
990
+ const walk = (nodes) => {
991
+ if (!nodes) {
992
+ return;
993
+ }
994
+ if (!Array.isArray(nodes)) {
995
+ this.log(`Warning: nodes is not an array: ${typeof nodes}, value: ${JSON.stringify(nodes)}`);
996
+ return;
997
+ }
998
+ for (const node of nodes) {
999
+ if (!node) {
1000
+ continue;
1001
+ }
1002
+ // Extract outputs from node
1003
+ if (node.outputs && typeof node.outputs === 'object') {
1004
+ for (const [key, value] of Object.entries(node.outputs)) {
1005
+ // Output name format: nodeId.outputKey (REQ-06)
1006
+ const outputName = `${node.id}.${key}`;
1007
+ outputs[outputName] = value;
1008
+ }
1009
+ }
1010
+ // Recursively walk children
1011
+ if (node.children) {
1012
+ if (Array.isArray(node.children) && node.children.length > 0) {
1013
+ walk(node.children);
1014
+ }
1015
+ }
1016
+ }
1017
+ };
1018
+ walk(cloudDOM);
1019
+ return outputs;
1020
+ }
1021
+ /**
1022
+ * Get the cloud provider (for testing/debugging)
1023
+ *
1024
+ * @returns The injected cloud provider
1025
+ */
1026
+ getCloudProvider() {
1027
+ return this.config.cloudProvider;
1028
+ }
1029
+ /**
1030
+ * Get the backend provider (for testing/debugging)
1031
+ *
1032
+ * @returns The injected backend provider
1033
+ */
1034
+ getBackendProvider() {
1035
+ return this.config.backendProvider;
1036
+ }
1037
+ /**
1038
+ * Get the state machine (for testing/debugging)
1039
+ *
1040
+ * @returns The state machine instance
1041
+ */
1042
+ getStateMachine() {
1043
+ return this.stateMachine;
1044
+ }
1045
+ /**
1046
+ * Get backend state for a stack (for CLI/comparison)
1047
+ *
1048
+ * @param stackName - Stack name to get state for
1049
+ * @returns Promise resolving to backend state or null
1050
+ */
1051
+ async getBackendState(stackName) {
1052
+ return this.stateMachine.getState(stackName);
1053
+ }
1054
+ /**
1055
+ * React-style render method (singleton API)
1056
+ *
1057
+ * Renders JSX to CloudDOM using the singleton configuration.
1058
+ * This is the recommended API for entry files.
1059
+ *
1060
+ * Example:
1061
+ * ```typescript
1062
+ * // Configure providers
1063
+ * CReact.cloudProvider = new AwsCloudProvider();
1064
+ * CReact.backendProvider = new S3BackendProvider();
1065
+ *
1066
+ * // Render app
1067
+ * export default CReact.renderCloudDOM(<MyApp />, 'my-stack');
1068
+ * ```
1069
+ *
1070
+ * @param element - JSX element to render
1071
+ * @param stackName - Stack name for state management
1072
+ * @returns Promise resolving to CloudDOM tree
1073
+ * @throws Error if providers are not configured
1074
+ */
1075
+ static async renderCloudDOM(element, stackName) {
1076
+ // Validate that providers are configured
1077
+ if (!CReact.cloudProvider) {
1078
+ throw new Error('CReact.cloudProvider must be set before calling renderCloudDOM.\n\n');
1079
+ }
1080
+ if (!CReact.backendProvider) {
1081
+ throw new Error('CReact.backendProvider must be set before calling renderCloudDOM.\n\n');
1082
+ }
1083
+ // Create instance with singleton configuration
1084
+ const instance = new CReact({
1085
+ cloudProvider: CReact.cloudProvider,
1086
+ backendProvider: CReact.backendProvider,
1087
+ migrationMap: CReact.migrationMap,
1088
+ asyncTimeout: CReact.asyncTimeout,
1089
+ });
1090
+ // Store the instance globally so CLI can access it
1091
+ CReact._lastInstance = instance;
1092
+ CReact._lastElement = element;
1093
+ CReact._lastStackName = stackName;
1094
+ // CRITICAL: Load previous state from backend before rendering
1095
+ // This enables useState to hydrate from persisted state
1096
+ await instance.loadStateForHydration(stackName);
1097
+ // Build and return CloudDOM
1098
+ return instance.build(element, stackName);
1099
+ }
1100
+ /**
1101
+ * Get the last instance created by renderCloudDOM (for CLI use)
1102
+ * @internal
1103
+ */
1104
+ static getLastInstance() {
1105
+ if (CReact._lastInstance) {
1106
+ return {
1107
+ instance: CReact._lastInstance,
1108
+ element: CReact._lastElement,
1109
+ stackName: CReact._lastStackName,
1110
+ };
1111
+ }
1112
+ return null;
1113
+ }
1114
+ }
1115
+ exports.CReact = CReact;
1116
+ // Re-export JSX functions for convenience (so users only need one import)
1117
+ CReact.createElement = jsx_1.CReact.createElement;
1118
+ CReact.Fragment = jsx_1.CReact.Fragment;
1119
+ // Global instance for hooks to access
1120
+ CReact.globalInstance = null;
1121
+ // CRITICAL: Hydration map must be STATIC (shared across all instances)
1122
+ // During hot reload, multiple CReact instances are created, but they all need
1123
+ // to share the same hydration data. The hydration is prepared on one instance
1124
+ // but useState (via getCReactInstance) accesses another instance.
1125
+ CReact.hydrationMap = new Map();
1126
+ // Export the getCReactInstance function for hooks to use
1127
+ exports.getCReactInstance = CReact.getCReactInstance;