@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,1037 @@
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.Reconciler = void 0;
33
+ exports.getTotalChanges = getTotalChanges;
34
+ exports.hasChanges = hasChanges;
35
+ const deepEqual_1 = require("../utils/deepEqual");
36
+ const errors_1 = require("./errors");
37
+ const Logger_1 = require("../utils/Logger");
38
+ const logger = Logger_1.LoggerFactory.getLogger('reconciler');
39
+ /**
40
+ * Helper function to get total number of changes from a ChangeSet
41
+ * Single source of truth for change counting
42
+ */
43
+ function getTotalChanges(changeSet) {
44
+ return (changeSet.creates.length +
45
+ changeSet.updates.length +
46
+ changeSet.deletes.length +
47
+ changeSet.replacements.length +
48
+ changeSet.moves.length);
49
+ }
50
+ /**
51
+ * Helper function to check if a ChangeSet has any changes
52
+ */
53
+ function hasChanges(changeSet) {
54
+ return getTotalChanges(changeSet) > 0;
55
+ }
56
+ /**
57
+ * Reconciler computes minimal change sets between CloudDOM states
58
+ *
59
+ * This is CReact's equivalent to React's Fiber reconciliation algorithm.
60
+ * It enables:
61
+ * - Incremental updates (only deploy what changed)
62
+ * - Plan preview (show diff before deploy)
63
+ * - Hot reload (apply deltas without full rebuild)
64
+ * - Dependency-aware ordering (deploy in correct order)
65
+ *
66
+ * REQ-O01: CloudDOM State Machine
67
+ * REQ-O04: Plan and Change Preview
68
+ */
69
+ class Reconciler {
70
+ constructor() {
71
+ /**
72
+ * Internal methods exposed for testing
73
+ *
74
+ * These are pure functions that are ideal for unit testing.
75
+ * Prefixed with __ to indicate they're internal/testing-only.
76
+ */
77
+ this.__testing__ = {
78
+ buildDependencyGraph: this.buildDependencyGraph.bind(this),
79
+ detectChangeType: this.detectChangeType.bind(this),
80
+ computeParallelBatches: this.computeParallelBatches.bind(this),
81
+ topologicalSort: this.topologicalSort.bind(this),
82
+ extractDependencies: this.extractDependencies.bind(this),
83
+ detectMoves: this.detectMoves.bind(this),
84
+ validateGraph: this.validateGraph.bind(this),
85
+ computeShallowHash: this.computeShallowHash.bind(this),
86
+ isMetadataKey: this.isMetadataKey.bind(this),
87
+ };
88
+ }
89
+ /**
90
+ * Generate a human-readable diff visualization for UI/CLI
91
+ *
92
+ * Formats the ChangeSet as JSON suitable for:
93
+ * - CLI diff previews (like Terraform plans)
94
+ * - Web UI diff visualization
95
+ * - CI/CD pipeline reports
96
+ *
97
+ * @param changeSet - ChangeSet to visualize
98
+ * @returns JSON-serializable diff visualization
99
+ */
100
+ generateDiffVisualization(changeSet) {
101
+ const changes = [];
102
+ // Add creates
103
+ for (const node of changeSet.creates) {
104
+ changes.push({
105
+ type: 'create',
106
+ nodeId: node.id,
107
+ details: {
108
+ construct: this.getConstructName(node.construct),
109
+ path: node.path,
110
+ },
111
+ });
112
+ }
113
+ // Add updates
114
+ for (const node of changeSet.updates) {
115
+ changes.push({
116
+ type: 'update',
117
+ nodeId: node.id,
118
+ details: {
119
+ construct: this.getConstructName(node.construct),
120
+ path: node.path,
121
+ },
122
+ });
123
+ }
124
+ // Add deletes
125
+ for (const node of changeSet.deletes) {
126
+ changes.push({
127
+ type: 'delete',
128
+ nodeId: node.id,
129
+ details: {
130
+ construct: this.getConstructName(node.construct),
131
+ path: node.path,
132
+ },
133
+ });
134
+ }
135
+ // Add replacements
136
+ for (const node of changeSet.replacements) {
137
+ changes.push({
138
+ type: 'replacement',
139
+ nodeId: node.id,
140
+ details: {
141
+ construct: this.getConstructName(node.construct),
142
+ path: node.path,
143
+ },
144
+ });
145
+ }
146
+ // Add moves
147
+ for (const move of changeSet.moves) {
148
+ changes.push({
149
+ type: 'move',
150
+ nodeId: move.nodeId,
151
+ details: {
152
+ from: move.from,
153
+ to: move.to,
154
+ },
155
+ });
156
+ }
157
+ // Format batches
158
+ const batches = changeSet.parallelBatches.map((batch, index) => ({
159
+ depth: index,
160
+ nodes: batch,
161
+ parallelism: batch.length,
162
+ }));
163
+ return {
164
+ summary: {
165
+ creates: changeSet.creates.length,
166
+ updates: changeSet.updates.length,
167
+ deletes: changeSet.deletes.length,
168
+ replacements: changeSet.replacements.length,
169
+ moves: changeSet.moves.length,
170
+ total: changes.length,
171
+ },
172
+ changes,
173
+ deployment: {
174
+ order: changeSet.deploymentOrder,
175
+ batches,
176
+ },
177
+ };
178
+ }
179
+ /**
180
+ * Debug logging helper
181
+ * Logs messages when CREACT_DEBUG environment variable is set
182
+ *
183
+ * Supports both string messages and structured data for telemetry.
184
+ * Includes timestamps for async reconciliation tracing.
185
+ */
186
+ log(message) {
187
+ if (typeof message === 'string') {
188
+ logger.debug(message);
189
+ }
190
+ else {
191
+ logger.debug(JSON.stringify(message, null, 2));
192
+ }
193
+ }
194
+ /**
195
+ * Check if a key is internal metadata (starts with underscore)
196
+ *
197
+ * Used for filtering metadata from prop comparisons and dependency scanning.
198
+ *
199
+ * @param key - Property key to check
200
+ * @returns True if key is metadata
201
+ */
202
+ isMetadataKey(key) {
203
+ return key.startsWith('_');
204
+ }
205
+ /**
206
+ * Compute a shallow hash of props for fast equality checks
207
+ *
208
+ * Creates a stable, key-order-independent hash by:
209
+ * - Filtering out special props (metadata, key, children)
210
+ * - Sorting entries by key
211
+ * - JSON stringifying the result
212
+ *
213
+ * This enables O(1) prop comparisons for unchanged nodes.
214
+ *
215
+ * @param props - Props object to hash
216
+ * @returns Stable hash string
217
+ */
218
+ computeShallowHash(props) {
219
+ if (!props || typeof props !== 'object') {
220
+ return 'null';
221
+ }
222
+ const entries = Object.entries(props)
223
+ .filter(([k]) => !this.isMetadataKey(k) && k !== 'key' && k !== 'children')
224
+ .sort(([a], [b]) => a.localeCompare(b));
225
+ try {
226
+ return JSON.stringify(entries);
227
+ }
228
+ catch {
229
+ // If serialization fails (circular refs, functions), use fallback
230
+ return `hash:${Math.random()}`;
231
+ }
232
+ }
233
+ /**
234
+ * Reconcile two CloudDOM states and compute minimal change set
235
+ *
236
+ * Algorithm:
237
+ * 1. Build ID maps for O(n) lookup
238
+ * 2. Detect creates (in current, not in previous)
239
+ * 3. Detect updates/replacements (in both, but props or construct changed)
240
+ * 4. Detect deletes (in previous, not in current)
241
+ * 5. Detect moves (nodes that changed parent)
242
+ * 6. Build dependency graph from current nodes
243
+ * 7. Compute topological sort for deployment order
244
+ * 8. Group independent resources into parallel batches
245
+ *
246
+ * Performance notes:
247
+ * - Synchronous for graphs <10k nodes
248
+ * - For larger graphs, consider async version with periodic yielding
249
+ * - Uses memoized deep equality for prop comparison
250
+ *
251
+ * REQ-O01: Diff algorithm for incremental updates
252
+ * REQ-O04: Change preview for plan command
253
+ *
254
+ * @param previous - Previous CloudDOM state
255
+ * @param current - Current CloudDOM state
256
+ * @returns ChangeSet with creates, updates, deletes, replacements, and deployment order
257
+ */
258
+ reconcile(previous, current) {
259
+ this.log('Starting reconciliation');
260
+ // Step 1: Build ID maps for O(n) lookup
261
+ const previousMap = this.buildNodeMap(previous);
262
+ const currentMap = this.buildNodeMap(current);
263
+ this.log(`Previous: ${previousMap.size} nodes, Current: ${currentMap.size} nodes`);
264
+ // Step 2: Detect creates (nodes in current but not in previous)
265
+ const creates = [];
266
+ for (const [id, node] of currentMap) {
267
+ if (!previousMap.has(id)) {
268
+ creates.push(node);
269
+ }
270
+ }
271
+ this.log(`Creates: ${creates.length} nodes`);
272
+ // Step 3: Detect updates and replacements (nodes in both with changes)
273
+ const updates = [];
274
+ const replacements = [];
275
+ for (const [id, currentNode] of currentMap) {
276
+ const previousNode = previousMap.get(id);
277
+ if (previousNode) {
278
+ const changeType = this.detectChangeType(previousNode, currentNode);
279
+ if (changeType === 'replacement') {
280
+ // Construct type changed - needs replacement (delete + create)
281
+ replacements.push(currentNode);
282
+ this.log(`Replacement detected: ${id} (construct changed)`);
283
+ }
284
+ else if (changeType === 'update') {
285
+ // Props changed but construct is same - can update in place
286
+ updates.push(currentNode);
287
+ this.log(`Update detected: ${id} (props changed)`);
288
+ }
289
+ // changeType === 'none' means no changes
290
+ }
291
+ }
292
+ this.log(`Updates: ${updates.length} nodes, Replacements: ${replacements.length} nodes`);
293
+ // Step 4: Detect deletes (nodes in previous but not in current)
294
+ const deletes = [];
295
+ for (const [id, node] of previousMap) {
296
+ if (!currentMap.has(id)) {
297
+ deletes.push(node);
298
+ }
299
+ }
300
+ this.log(`Deletes: ${deletes.length} nodes`);
301
+ // Step 4.5: Detect moves (nodes that changed parent in hierarchy)
302
+ const moves = this.detectMoves(previousMap, currentMap);
303
+ this.log(`Moves: ${moves.length} nodes`);
304
+ // Debug logging: breakdown of change types (CREACT_DEBUG only)
305
+ this.log('\n🔍 Reconciliation Change Breakdown:');
306
+ this.log(` Creates: ${creates.length} nodes - ${creates.map((n) => n.id).join(', ') || 'none'}`);
307
+ this.log(` Updates: ${updates.length} nodes - ${updates.map((n) => n.id).join(', ') || 'none'}`);
308
+ this.log(` Deletes: ${deletes.length} nodes - ${deletes.map((n) => n.id).join(', ') || 'none'}`);
309
+ this.log(` Replacements: ${replacements.length} nodes - ${replacements.map((n) => n.id).join(', ') || 'none'}`);
310
+ this.log(` Moves: ${moves.length} nodes`);
311
+ if (moves.length > 0) {
312
+ this.log(' Move details:');
313
+ moves.forEach((move) => {
314
+ this.log(` ${move.nodeId}: "${move.from}" → "${move.to}"`);
315
+ });
316
+ }
317
+ // Calculate unchanged nodes for idempotency verification
318
+ const unchanged = currentMap.size - (creates.length + updates.length + replacements.length);
319
+ this.log(`Unchanged: ${unchanged} nodes (idempotent)`);
320
+ // Step 5: Build dependency graph from current nodes
321
+ this.log('Building dependency graph');
322
+ const graph = this.buildDependencyGraph(Array.from(currentMap.values()));
323
+ // Step 6: Compute topological sort for deployment order
324
+ this.log('Computing deployment order');
325
+ const deploymentOrder = this.topologicalSort(graph);
326
+ // Step 7: Compute parallel deployment batches
327
+ this.log('Computing parallel batches');
328
+ const parallelBatches = this.computeParallelBatches(deploymentOrder, graph);
329
+ this.log(`Deployment order: ${deploymentOrder.length} nodes in ${parallelBatches.length} batches`);
330
+ // Structured logging for telemetry/debugging
331
+ this.log({
332
+ phase: 'reconciliation_complete',
333
+ summary: {
334
+ creates: creates.length,
335
+ updates: updates.length,
336
+ deletes: deletes.length,
337
+ replacements: replacements.length,
338
+ unchanged,
339
+ total: currentMap.size,
340
+ },
341
+ deployment: {
342
+ order: deploymentOrder,
343
+ batches: parallelBatches.length,
344
+ maxParallelism: Math.max(...parallelBatches.map((b) => b.length), 0),
345
+ },
346
+ });
347
+ return {
348
+ creates,
349
+ updates,
350
+ deletes,
351
+ replacements,
352
+ moves,
353
+ deploymentOrder,
354
+ parallelBatches,
355
+ };
356
+ }
357
+ /**
358
+ * Async reconcile for large CloudDOM graphs (>10k nodes)
359
+ *
360
+ * Yields periodically to prevent blocking the event loop.
361
+ * Useful for:
362
+ * - Large infrastructure graphs
363
+ * - UI responsiveness during diff computation
364
+ * - Long-running CI/CD pipelines
365
+ *
366
+ * Algorithm is identical to synchronous reconcile, but yields every N nodes.
367
+ *
368
+ * @param previous - Previous CloudDOM state
369
+ * @param current - Current CloudDOM state
370
+ * @param yieldInterval - Number of nodes to process before yielding (default: 1000)
371
+ * @returns Promise resolving to ChangeSet
372
+ */
373
+ async reconcileAsync(previous, current, yieldInterval = 1000) {
374
+ this.log('Starting async reconciliation');
375
+ // Step 1: Build ID maps for O(n) lookup
376
+ const previousMap = this.buildNodeMap(previous);
377
+ const currentMap = this.buildNodeMap(current);
378
+ this.log(`Previous: ${previousMap.size} nodes, Current: ${currentMap.size} nodes`);
379
+ // Step 2: Detect creates (with periodic yielding)
380
+ const creates = [];
381
+ let processedCount = 0;
382
+ for (const [id, node] of currentMap) {
383
+ if (!previousMap.has(id)) {
384
+ creates.push(node);
385
+ }
386
+ // Yield periodically to prevent blocking
387
+ if (++processedCount % yieldInterval === 0) {
388
+ await this.yield();
389
+ }
390
+ }
391
+ this.log(`Creates: ${creates.length} nodes`);
392
+ // Step 3: Detect updates and replacements (with periodic yielding)
393
+ const updates = [];
394
+ const replacements = [];
395
+ processedCount = 0;
396
+ for (const [id, currentNode] of currentMap) {
397
+ const previousNode = previousMap.get(id);
398
+ if (previousNode) {
399
+ const changeType = this.detectChangeType(previousNode, currentNode);
400
+ if (changeType === 'replacement') {
401
+ replacements.push(currentNode);
402
+ this.log(`Replacement detected: ${id} (construct changed)`);
403
+ }
404
+ else if (changeType === 'update') {
405
+ updates.push(currentNode);
406
+ this.log(`Update detected: ${id} (props changed)`);
407
+ }
408
+ }
409
+ // Yield periodically
410
+ if (++processedCount % yieldInterval === 0) {
411
+ await this.yield();
412
+ }
413
+ }
414
+ this.log(`Updates: ${updates.length} nodes, Replacements: ${replacements.length} nodes`);
415
+ // Step 4: Detect deletes
416
+ const deletes = [];
417
+ for (const [id, node] of previousMap) {
418
+ if (!currentMap.has(id)) {
419
+ deletes.push(node);
420
+ }
421
+ }
422
+ this.log(`Deletes: ${deletes.length} nodes`);
423
+ // Step 4.5: Detect moves
424
+ const moves = this.detectMoves(previousMap, currentMap);
425
+ this.log(`Moves: ${moves.length} nodes`);
426
+ // Calculate unchanged nodes
427
+ const unchanged = currentMap.size - (creates.length + updates.length + replacements.length);
428
+ this.log(`Unchanged: ${unchanged} nodes (idempotent)`);
429
+ // Step 5: Build dependency graph
430
+ this.log('Building dependency graph');
431
+ const graph = this.buildDependencyGraph(Array.from(currentMap.values()));
432
+ // Step 6: Compute topological sort
433
+ this.log('Computing deployment order');
434
+ const deploymentOrder = this.topologicalSort(graph);
435
+ // Step 7: Compute parallel batches
436
+ this.log('Computing parallel batches');
437
+ const parallelBatches = this.computeParallelBatches(deploymentOrder, graph);
438
+ this.log(`Deployment order: ${deploymentOrder.length} nodes in ${parallelBatches.length} batches`);
439
+ // Structured logging
440
+ this.log({
441
+ phase: 'async_reconciliation_complete',
442
+ summary: {
443
+ creates: creates.length,
444
+ updates: updates.length,
445
+ deletes: deletes.length,
446
+ replacements: replacements.length,
447
+ unchanged,
448
+ total: currentMap.size,
449
+ },
450
+ deployment: {
451
+ order: deploymentOrder,
452
+ batches: parallelBatches.length,
453
+ maxParallelism: Math.max(...parallelBatches.map((b) => b.length), 0),
454
+ },
455
+ });
456
+ return {
457
+ creates,
458
+ updates,
459
+ deletes,
460
+ replacements,
461
+ moves,
462
+ deploymentOrder,
463
+ parallelBatches,
464
+ };
465
+ }
466
+ /**
467
+ * Yield control to event loop
468
+ *
469
+ * Uses setImmediate in Node.js, setTimeout in browser.
470
+ * Prevents blocking during large graph reconciliation.
471
+ */
472
+ yield() {
473
+ return new Promise((resolve) => {
474
+ if (typeof setImmediate !== 'undefined') {
475
+ setImmediate(resolve);
476
+ }
477
+ else {
478
+ setTimeout(resolve, 0);
479
+ }
480
+ });
481
+ }
482
+ /**
483
+ * Build a flat map of node ID → CloudDOMNode for O(n) lookup
484
+ *
485
+ * Recursively walks the CloudDOM tree and collects all nodes.
486
+ *
487
+ * @param nodes - CloudDOM tree (root nodes)
488
+ * @returns Map of node ID → CloudDOMNode
489
+ */
490
+ buildNodeMap(nodes) {
491
+ const map = new Map();
492
+ const walk = (node) => {
493
+ map.set(node.id, node);
494
+ if (node.children && node.children.length > 0) {
495
+ node.children.forEach(walk);
496
+ }
497
+ };
498
+ nodes.forEach(walk);
499
+ return map;
500
+ }
501
+ /**
502
+ * Detect moves (nodes that changed parent in hierarchy)
503
+ *
504
+ * A move is detected when:
505
+ * - Node exists in both previous and current
506
+ * - Node's parent path changed (using array equality, not string comparison)
507
+ *
508
+ * This is useful for hierarchical updates where resources move
509
+ * between parent containers.
510
+ *
511
+ * @param previousMap - Previous node map
512
+ * @param currentMap - Current node map
513
+ * @returns Array of move operations with node ID
514
+ */
515
+ detectMoves(previousMap, currentMap) {
516
+ const moves = [];
517
+ for (const [id, currentNode] of currentMap) {
518
+ const previousNode = previousMap.get(id);
519
+ if (previousNode) {
520
+ // Get parent paths (all but last segment)
521
+ const prevParentPathArray = previousNode.path.slice(0, -1);
522
+ const currParentPathArray = currentNode.path.slice(0, -1);
523
+ // CRITICAL FIX: Handle empty parent paths (root nodes)
524
+ // Both nodes at root level (empty parent paths) - no move
525
+ if (prevParentPathArray.length === 0 && currParentPathArray.length === 0) {
526
+ this.log(`Skipping move detection for root node: ${id}`);
527
+ continue;
528
+ }
529
+ // Check if parent changed using array equality (not string comparison)
530
+ // This handles dynamic segments correctly
531
+ if (!(0, deepEqual_1.deepEqual)(prevParentPathArray, currParentPathArray, false)) {
532
+ const prevParentPath = prevParentPathArray.join('.') || '<root>';
533
+ const currParentPath = currParentPathArray.join('.') || '<root>';
534
+ // Only add if paths are actually different strings
535
+ if (prevParentPath !== currParentPath) {
536
+ moves.push({
537
+ nodeId: id,
538
+ from: prevParentPath,
539
+ to: currParentPath,
540
+ });
541
+ this.log(`Move detected: ${id} from ${prevParentPath} to ${currParentPath}`);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ return moves;
547
+ }
548
+ /**
549
+ * Detect the type of change between two nodes
550
+ *
551
+ * Returns:
552
+ * - 'replacement': Construct type changed (needs delete + create)
553
+ * - 'update': Props changed but construct is same (can update in place)
554
+ * - 'none': No changes detected
555
+ *
556
+ * Performance optimization:
557
+ * - Uses shallow prop hashes for O(1) comparison
558
+ * - Only falls back to deep equality if hashes differ
559
+ * - Caches hashes in node metadata (_propHash)
560
+ *
561
+ * NOTE: The `state` field is intentionally NOT compared during reconciliation.
562
+ * Only `props` and `outputs` are compared. The `state` field contains useState
563
+ * values which should not trigger infrastructure updates. This separation ensures
564
+ * that application state changes don't cause unnecessary resource deployments.
565
+ *
566
+ * @param previous - Previous node
567
+ * @param current - Current node
568
+ * @returns Change type
569
+ */
570
+ detectChangeType(previous, current) {
571
+ var _a, _b;
572
+ // Check if construct type changed (needs replacement)
573
+ // Use constructType string field for reliable comparison after serialization
574
+ const prevType = previous.constructType || this.getConstructName(previous.construct);
575
+ const currType = current.constructType || this.getConstructName(current.construct);
576
+ if (prevType !== currType) {
577
+ logger.debug(`Construct type mismatch for ${current.id}:`, {
578
+ previousType: prevType,
579
+ currentType: currType,
580
+ });
581
+ return 'replacement';
582
+ }
583
+ // Hash-based diff acceleration: compute or reuse cached hashes
584
+ const prevHash = ((_a = previous)._propHash ?? (_a._propHash = this.computeShallowHash(previous.props)));
585
+ const currHash = ((_b = current)._propHash ?? (_b._propHash = this.computeShallowHash(current.props)));
586
+ logger.debug(`Hash comparison for ${current.id}:`, {
587
+ prevHash,
588
+ currHash,
589
+ match: prevHash === currHash,
590
+ prevProps: previous.props,
591
+ currProps: current.props,
592
+ });
593
+ // Fast path: if hashes match, props are identical (skip deep equality)
594
+ if (prevHash === currHash) {
595
+ // Props are identical - no change needed
596
+ // NOTE: outputs are ignored per REQ-R03 (runtime values only)
597
+ return 'none';
598
+ }
599
+ // Hashes differ: perform deep equality check to confirm
600
+ // NOTE: Only compares `props` field, NOT `outputs` or `state` fields
601
+ // outputs are runtime values and should not trigger reconciliation (REQ-R03)
602
+ if (this.propsChanged(previous.props, current.props)) {
603
+ return 'update';
604
+ }
605
+ return 'none';
606
+ }
607
+ /**
608
+ * Check if two constructs are equal
609
+ *
610
+ * Constructs are considered equal if they have the same name.
611
+ * Handles edge cases like minified builds where function names may be undefined.
612
+ *
613
+ * NOTE: After serialization, construct field may be undefined, so we rely on
614
+ * constructType string field for comparison.
615
+ *
616
+ * @param a - First construct
617
+ * @param b - Second construct
618
+ * @returns True if constructs are equal
619
+ */
620
+ constructsEqual(a, b) {
621
+ // Handle null/undefined
622
+ if (a === b) {
623
+ return true;
624
+ }
625
+ if (!a || !b) {
626
+ return false;
627
+ }
628
+ // Extract names for comparison
629
+ const nameA = this.getConstructName(a);
630
+ const nameB = this.getConstructName(b);
631
+ return nameA === nameB;
632
+ }
633
+ /**
634
+ * Get the name of a construct
635
+ *
636
+ * @param construct - Construct to get name from
637
+ * @returns Construct name
638
+ */
639
+ getConstructName(construct) {
640
+ if (typeof construct === 'string') {
641
+ return construct;
642
+ }
643
+ if (typeof construct === 'function') {
644
+ return construct.name || construct.constructor?.name || 'anonymous';
645
+ }
646
+ if (construct && typeof construct === 'object' && 'name' in construct) {
647
+ return String(construct.name);
648
+ }
649
+ return 'unknown';
650
+ }
651
+ /**
652
+ * Check if node props have changed using deep equality
653
+ *
654
+ * Compares props objects deeply to detect any changes.
655
+ * Uses the deepEqual utility with memoization for performance.
656
+ *
657
+ * Excludes special props that don't affect rendering:
658
+ * - Metadata props (starting with _)
659
+ * - key prop (used for identity, not rendering)
660
+ * - children prop (handled separately in rendering)
661
+ *
662
+ * @param prevProps - Previous props (optional, defaults to empty object)
663
+ * @param currProps - Current props (optional, defaults to empty object)
664
+ * @returns True if props have changed
665
+ */
666
+ propsChanged(prevProps = {}, currProps = {}) {
667
+ // Filter out special props that don't affect rendering
668
+ const filterSpecialProps = (props) => {
669
+ const filtered = {};
670
+ for (const [key, value] of Object.entries(props)) {
671
+ // Exclude metadata (starts with _)
672
+ if (this.isMetadataKey(key)) {
673
+ continue;
674
+ }
675
+ // Exclude key (used for identity, not rendering)
676
+ if (key === 'key') {
677
+ continue;
678
+ }
679
+ // Exclude children (handled separately)
680
+ if (key === 'children') {
681
+ continue;
682
+ }
683
+ filtered[key] = value;
684
+ }
685
+ return filtered;
686
+ };
687
+ const prevFiltered = filterSpecialProps(prevProps);
688
+ const currFiltered = filterSpecialProps(currProps);
689
+ // Use deep equality with memoization
690
+ return !(0, deepEqual_1.deepEqual)(prevFiltered, currFiltered);
691
+ }
692
+ /**
693
+ * Check if outputs have changed using deep equality
694
+ *
695
+ * Treats empty objects ({}) as equivalent to undefined for idempotency.
696
+ *
697
+ * NOTE: This method only receives and compares the `outputs` field from CloudDOMNode.
698
+ * The `state` field is never passed to this method, ensuring that useState values
699
+ * do not trigger infrastructure updates. This separation is automatic - callers
700
+ * explicitly pass `node.outputs`, not `node.state`.
701
+ *
702
+ * @param previous - Previous outputs (from node.outputs field only)
703
+ * @param current - Current outputs (from node.outputs field only)
704
+ * @returns True if outputs changed
705
+ */
706
+ outputsChanged(previous, current) {
707
+ // Helper to check if output is empty (undefined or {})
708
+ const isEmpty = (output) => {
709
+ return (output === undefined || (typeof output === 'object' && Object.keys(output).length === 0));
710
+ };
711
+ const prevIsEmpty = isEmpty(previous);
712
+ const currIsEmpty = isEmpty(current);
713
+ // CRITICAL FIX: Both empty - no change
714
+ if (prevIsEmpty && currIsEmpty) {
715
+ return false;
716
+ }
717
+ // One empty, one not - changed
718
+ if (prevIsEmpty !== currIsEmpty) {
719
+ return true;
720
+ }
721
+ // Both have content - use deep equality
722
+ return !(0, deepEqual_1.deepEqual)(previous, current, true);
723
+ }
724
+ /**
725
+ * Build dependency graph from CloudDOM nodes
726
+ *
727
+ * Scans node props for references to other node IDs and builds
728
+ * an adjacency list representing dependencies.
729
+ *
730
+ * A node depends on another if its props reference the other node's ID.
731
+ *
732
+ * REQ-O01: Dependency graph for deployment ordering
733
+ *
734
+ * @param nodes - CloudDOM nodes
735
+ * @returns DependencyGraph with adjacency lists
736
+ */
737
+ buildDependencyGraph(nodes) {
738
+ const dependencies = new Map();
739
+ const dependents = new Map();
740
+ // Initialize empty dependency lists for all nodes
741
+ for (const node of nodes) {
742
+ dependencies.set(node.id, []);
743
+ dependents.set(node.id, []);
744
+ }
745
+ // Build set of all node IDs for quick lookup
746
+ const nodeIds = new Set(nodes.map((n) => n.id));
747
+ // Scan props for references to other node IDs
748
+ for (const node of nodes) {
749
+ const deps = this.extractDependencies(node, nodeIds);
750
+ dependencies.set(node.id, deps);
751
+ // Build reverse adjacency list (dependents)
752
+ for (const depId of deps) {
753
+ if (!dependents.has(depId)) {
754
+ dependents.set(depId, []);
755
+ }
756
+ dependents.get(depId).push(node.id);
757
+ }
758
+ }
759
+ // Validate graph integrity
760
+ this.validateGraph({ dependencies, dependents });
761
+ // Detect circular dependencies
762
+ this.detectCircularDependencies(dependencies);
763
+ return { dependencies, dependents };
764
+ }
765
+ /**
766
+ * Validate dependency graph integrity
767
+ *
768
+ * Ensures all dependencies exist in the graph and no missing node IDs remain.
769
+ *
770
+ * @param graph - Dependency graph to validate
771
+ * @throws ReconciliationError if graph is invalid
772
+ */
773
+ validateGraph(graph) {
774
+ const { dependencies } = graph;
775
+ for (const [nodeId, deps] of dependencies) {
776
+ for (const depId of deps) {
777
+ if (!dependencies.has(depId)) {
778
+ const errorDetails = {
779
+ nodeId,
780
+ missingDependency: depId,
781
+ availableNodes: Array.from(dependencies.keys()),
782
+ };
783
+ throw new errors_1.ReconciliationError(`Missing dependency: ${nodeId} → ${depId}. Referenced node does not exist in graph.`, errorDetails);
784
+ }
785
+ }
786
+ }
787
+ }
788
+ /**
789
+ * Extract dependencies from a node's props
790
+ *
791
+ * Recursively scans props object for strings that match other node IDs.
792
+ *
793
+ * Optimizations:
794
+ * - Skips props that start with underscore (internal metadata)
795
+ * - Uses explicit ref convention when available (props.ref or props.dependsOn)
796
+ * - Caches known non-ID props to avoid false positives
797
+ *
798
+ * @param node - CloudDOM node
799
+ * @param nodeIds - Set of all node IDs for validation
800
+ * @returns Array of dependency IDs
801
+ */
802
+ extractDependencies(node, nodeIds) {
803
+ const deps = new Set();
804
+ // Check for explicit dependency declarations first
805
+ if (node.props.dependsOn) {
806
+ const dependsOn = Array.isArray(node.props.dependsOn)
807
+ ? node.props.dependsOn
808
+ : [node.props.dependsOn];
809
+ for (const depId of dependsOn) {
810
+ if (typeof depId === 'string' && nodeIds.has(depId) && depId !== node.id) {
811
+ deps.add(depId);
812
+ }
813
+ }
814
+ }
815
+ // Check for ref prop (common convention)
816
+ if (node.props.ref && typeof node.props.ref === 'string') {
817
+ if (nodeIds.has(node.props.ref) && node.props.ref !== node.id) {
818
+ deps.add(node.props.ref);
819
+ }
820
+ }
821
+ // Scan all props for implicit dependencies
822
+ const scan = (value, key) => {
823
+ // Skip internal metadata props using shared helper
824
+ if (key && this.isMetadataKey(key)) {
825
+ return;
826
+ }
827
+ // Skip known non-ID props
828
+ if (key && this.isKnownNonIdProp(key)) {
829
+ return;
830
+ }
831
+ if (typeof value === 'string') {
832
+ // Check if this string is a node ID
833
+ if (nodeIds.has(value) && value !== node.id) {
834
+ deps.add(value);
835
+ }
836
+ }
837
+ else if (Array.isArray(value)) {
838
+ value.forEach((v) => scan(v));
839
+ }
840
+ else if (value && typeof value === 'object') {
841
+ for (const [k, v] of Object.entries(value)) {
842
+ scan(v, k);
843
+ }
844
+ }
845
+ };
846
+ // Scan all props
847
+ for (const [key, value] of Object.entries(node.props)) {
848
+ scan(value, key);
849
+ }
850
+ return Array.from(deps);
851
+ }
852
+ /**
853
+ * Check if a prop key is known to never contain node IDs
854
+ *
855
+ * This helps avoid false positives in dependency extraction.
856
+ *
857
+ * @param key - Prop key
858
+ * @returns True if this prop is known to not contain IDs
859
+ */
860
+ isKnownNonIdProp(key) {
861
+ const knownNonIdProps = new Set([
862
+ 'name',
863
+ 'description',
864
+ 'version',
865
+ 'region',
866
+ 'zone',
867
+ 'namespace',
868
+ 'label',
869
+ 'tag',
870
+ 'tags',
871
+ 'metadata',
872
+ 'annotations',
873
+ 'key',
874
+ ]);
875
+ return knownNonIdProps.has(key);
876
+ }
877
+ /**
878
+ * Detect circular dependencies using DFS
879
+ *
880
+ * Collects all cycles before throwing to provide comprehensive diagnostics.
881
+ *
882
+ * REQ-O01: Circular dependency detection
883
+ *
884
+ * @param dependencies - Dependency adjacency list
885
+ * @throws ReconciliationError if circular dependencies are detected
886
+ */
887
+ detectCircularDependencies(dependencies) {
888
+ const visited = new Set();
889
+ const stack = new Set();
890
+ const cycles = [];
891
+ const dfs = (nodeId, path = []) => {
892
+ if (stack.has(nodeId)) {
893
+ // Circular dependency detected - collect it
894
+ const cycle = [...path, nodeId];
895
+ cycles.push(cycle);
896
+ return;
897
+ }
898
+ if (visited.has(nodeId)) {
899
+ return;
900
+ }
901
+ visited.add(nodeId);
902
+ stack.add(nodeId);
903
+ const deps = dependencies.get(nodeId) || [];
904
+ for (const depId of deps) {
905
+ try {
906
+ dfs(depId, [...path, nodeId]);
907
+ }
908
+ catch {
909
+ // Continue checking other dependencies
910
+ }
911
+ }
912
+ stack.delete(nodeId);
913
+ };
914
+ // Run DFS from each node to find all cycles
915
+ for (const nodeId of dependencies.keys()) {
916
+ if (!visited.has(nodeId)) {
917
+ dfs(nodeId);
918
+ }
919
+ }
920
+ // Throw if any cycles were found
921
+ if (cycles.length > 0) {
922
+ const cycleStrings = cycles.map((cycle) => cycle.join(' → '));
923
+ throw new errors_1.ReconciliationError(`Circular dependencies detected:\n ${cycleStrings.join('\n ')}`, {
924
+ cycles,
925
+ count: cycles.length,
926
+ });
927
+ }
928
+ }
929
+ /**
930
+ * Compute topological sort for deployment order using Kahn's algorithm
931
+ *
932
+ * Returns array of node IDs in deployment order (dependencies first).
933
+ * Nodes with same depth are sorted by ID for determinism.
934
+ *
935
+ * REQ-O01: Topological sort for deployment order
936
+ *
937
+ * @param graph - Dependency graph
938
+ * @returns Array of node IDs in deployment order
939
+ */
940
+ topologicalSort(graph) {
941
+ const { dependencies, dependents } = graph;
942
+ // Calculate in-degree for each node (number of dependencies)
943
+ const inDegree = new Map();
944
+ for (const [nodeId, deps] of dependencies) {
945
+ inDegree.set(nodeId, deps.length);
946
+ }
947
+ // Start with nodes that have no dependencies (in-degree = 0)
948
+ const queue = [];
949
+ for (const [nodeId, degree] of inDegree) {
950
+ if (degree === 0) {
951
+ queue.push(nodeId);
952
+ }
953
+ }
954
+ // Sort queue for determinism
955
+ queue.sort();
956
+ const result = [];
957
+ while (queue.length > 0) {
958
+ // Process nodes at same depth in sorted order for determinism
959
+ const batchSize = queue.length;
960
+ const batch = [];
961
+ for (let i = 0; i < batchSize; i++) {
962
+ const nodeId = queue.shift();
963
+ batch.push(nodeId);
964
+ // Reduce in-degree of dependents
965
+ const deps = dependents.get(nodeId) || [];
966
+ for (const depId of deps) {
967
+ const degree = inDegree.get(depId) - 1;
968
+ inDegree.set(depId, degree);
969
+ if (degree === 0) {
970
+ queue.push(depId);
971
+ }
972
+ }
973
+ }
974
+ // Sort batch for determinism and add to result
975
+ batch.sort();
976
+ result.push(...batch);
977
+ // Sort queue for next iteration
978
+ queue.sort();
979
+ }
980
+ // Safety guard: ensure all nodes were sorted
981
+ if (result.length !== dependencies.size) {
982
+ throw new errors_1.ReconciliationError(`Topological sort incomplete: ${result.length}/${dependencies.size} nodes sorted. Possible cycle or missing dependency.`, {
983
+ sorted: result,
984
+ expected: dependencies.size,
985
+ missing: Array.from(dependencies.keys()).filter((id) => !result.includes(id)),
986
+ });
987
+ }
988
+ return result;
989
+ }
990
+ /**
991
+ * Compute parallel deployment batches
992
+ *
993
+ * Groups nodes by depth in dependency graph.
994
+ * Nodes at same depth can deploy in parallel (no dependencies between them).
995
+ *
996
+ * REQ-O01: Parallel deployment batches
997
+ *
998
+ * @param deploymentOrder - Topologically sorted node IDs
999
+ * @param graph - Dependency graph
1000
+ * @returns Array of batches (each batch can deploy in parallel)
1001
+ */
1002
+ computeParallelBatches(deploymentOrder, graph) {
1003
+ const { dependencies } = graph;
1004
+ // Calculate depth for each node (max distance from a root node)
1005
+ const depths = new Map();
1006
+ for (const nodeId of deploymentOrder) {
1007
+ const deps = dependencies.get(nodeId) || [];
1008
+ if (deps.length === 0) {
1009
+ // Root node (no dependencies)
1010
+ depths.set(nodeId, 0);
1011
+ }
1012
+ else {
1013
+ // Depth = max(dependency depths) + 1
1014
+ const maxDepth = Math.max(...deps.map((depId) => depths.get(depId) || 0));
1015
+ depths.set(nodeId, maxDepth + 1);
1016
+ }
1017
+ }
1018
+ // Group nodes by depth
1019
+ const batches = new Map();
1020
+ for (const [nodeId, depth] of depths) {
1021
+ if (!batches.has(depth)) {
1022
+ batches.set(depth, []);
1023
+ }
1024
+ batches.get(depth).push(nodeId);
1025
+ }
1026
+ // Convert to array and sort each batch for determinism
1027
+ const result = [];
1028
+ const sortedDepths = Array.from(batches.keys()).sort((a, b) => a - b);
1029
+ for (const depth of sortedDepths) {
1030
+ const batch = batches.get(depth);
1031
+ batch.sort(); // Sort for determinism
1032
+ result.push(batch);
1033
+ }
1034
+ return result;
1035
+ }
1036
+ }
1037
+ exports.Reconciler = Reconciler;