@creact-labs/creact 0.1.8 → 0.2.1

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