@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.
- package/LICENSE +212 -0
- package/README.md +379 -0
- package/dist/cli/commands/BuildCommand.d.ts +40 -0
- package/dist/cli/commands/BuildCommand.js +151 -0
- package/dist/cli/commands/DeployCommand.d.ts +38 -0
- package/dist/cli/commands/DeployCommand.js +194 -0
- package/dist/cli/commands/DevCommand.d.ts +52 -0
- package/dist/cli/commands/DevCommand.js +385 -0
- package/dist/cli/commands/PlanCommand.d.ts +39 -0
- package/dist/cli/commands/PlanCommand.js +164 -0
- package/dist/cli/commands/index.d.ts +36 -0
- package/dist/cli/commands/index.js +43 -0
- package/dist/cli/core/ArgumentParser.d.ts +46 -0
- package/dist/cli/core/ArgumentParser.js +127 -0
- package/dist/cli/core/BaseCommand.d.ts +75 -0
- package/dist/cli/core/BaseCommand.js +95 -0
- package/dist/cli/core/CLIContext.d.ts +68 -0
- package/dist/cli/core/CLIContext.js +183 -0
- package/dist/cli/core/CommandRegistry.d.ts +64 -0
- package/dist/cli/core/CommandRegistry.js +89 -0
- package/dist/cli/core/index.d.ts +36 -0
- package/dist/cli/core/index.js +43 -0
- package/dist/cli/index.d.ts +35 -0
- package/dist/cli/index.js +100 -0
- package/dist/cli/output.d.ts +204 -0
- package/dist/cli/output.js +437 -0
- package/dist/cli/utils.d.ts +59 -0
- package/dist/cli/utils.js +76 -0
- package/dist/context/createContext.d.ts +90 -0
- package/dist/context/createContext.js +113 -0
- package/dist/context/index.d.ts +30 -0
- package/dist/context/index.js +35 -0
- package/dist/core/CReact.d.ts +409 -0
- package/dist/core/CReact.js +1127 -0
- package/dist/core/CloudDOMBuilder.d.ts +429 -0
- package/dist/core/CloudDOMBuilder.js +1198 -0
- package/dist/core/ContextDependencyTracker.d.ts +165 -0
- package/dist/core/ContextDependencyTracker.js +448 -0
- package/dist/core/ErrorRecoveryManager.d.ts +145 -0
- package/dist/core/ErrorRecoveryManager.js +443 -0
- package/dist/core/EventBus.d.ts +91 -0
- package/dist/core/EventBus.js +185 -0
- package/dist/core/ProviderOutputTracker.d.ts +211 -0
- package/dist/core/ProviderOutputTracker.js +476 -0
- package/dist/core/ReactiveUpdateQueue.d.ts +76 -0
- package/dist/core/ReactiveUpdateQueue.js +121 -0
- package/dist/core/Reconciler.d.ts +415 -0
- package/dist/core/Reconciler.js +1037 -0
- package/dist/core/RenderScheduler.d.ts +153 -0
- package/dist/core/RenderScheduler.js +519 -0
- package/dist/core/Renderer.d.ts +276 -0
- package/dist/core/Renderer.js +791 -0
- package/dist/core/Runtime.d.ts +246 -0
- package/dist/core/Runtime.js +640 -0
- package/dist/core/StateBindingManager.d.ts +121 -0
- package/dist/core/StateBindingManager.js +309 -0
- package/dist/core/StateMachine.d.ts +424 -0
- package/dist/core/StateMachine.js +787 -0
- package/dist/core/StructuralChangeDetector.d.ts +140 -0
- package/dist/core/StructuralChangeDetector.js +363 -0
- package/dist/core/Validator.d.ts +127 -0
- package/dist/core/Validator.js +279 -0
- package/dist/core/errors.d.ts +153 -0
- package/dist/core/errors.js +202 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +64 -0
- package/dist/core/types.d.ts +263 -0
- package/dist/core/types.js +48 -0
- package/dist/hooks/context.d.ts +147 -0
- package/dist/hooks/context.js +334 -0
- package/dist/hooks/useContext.d.ts +113 -0
- package/dist/hooks/useContext.js +169 -0
- package/dist/hooks/useEffect.d.ts +105 -0
- package/dist/hooks/useEffect.js +540 -0
- package/dist/hooks/useInstance.d.ts +139 -0
- package/dist/hooks/useInstance.js +441 -0
- package/dist/hooks/useState.d.ts +120 -0
- package/dist/hooks/useState.js +298 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +70 -0
- package/dist/jsx.d.ts +64 -0
- package/dist/jsx.js +76 -0
- package/dist/providers/DummyBackendProvider.d.ts +193 -0
- package/dist/providers/DummyBackendProvider.js +189 -0
- package/dist/providers/DummyCloudProvider.d.ts +128 -0
- package/dist/providers/DummyCloudProvider.js +157 -0
- package/dist/providers/IBackendProvider.d.ts +177 -0
- package/dist/providers/IBackendProvider.js +31 -0
- package/dist/providers/ICloudProvider.d.ts +146 -0
- package/dist/providers/ICloudProvider.js +31 -0
- package/dist/providers/index.d.ts +31 -0
- package/dist/providers/index.js +31 -0
- package/dist/test-event-callbacks.d.ts +0 -0
- package/dist/test-event-callbacks.js +1 -0
- package/dist/utils/Logger.d.ts +144 -0
- package/dist/utils/Logger.js +220 -0
- package/dist/utils/Output.d.ts +161 -0
- package/dist/utils/Output.js +401 -0
- package/dist/utils/deepEqual.d.ts +71 -0
- package/dist/utils/deepEqual.js +276 -0
- package/dist/utils/naming.d.ts +241 -0
- package/dist/utils/naming.js +376 -0
- package/package.json +87 -0
|
@@ -0,0 +1,640 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
32
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.Runtime = void 0;
|
|
36
|
+
// Runtime class for execution management
|
|
37
|
+
// REQ-1.1 through REQ-1.9: Runtime class with lifecycle management
|
|
38
|
+
const Renderer_1 = require("./Renderer");
|
|
39
|
+
const Validator_1 = require("./Validator");
|
|
40
|
+
const CloudDOMBuilder_1 = require("./CloudDOMBuilder");
|
|
41
|
+
const StateMachine_1 = require("./StateMachine");
|
|
42
|
+
const Reconciler_1 = require("./Reconciler");
|
|
43
|
+
const RenderScheduler_1 = require("./RenderScheduler");
|
|
44
|
+
const StateBindingManager_1 = require("./StateBindingManager");
|
|
45
|
+
const ProviderOutputTracker_1 = require("./ProviderOutputTracker");
|
|
46
|
+
const ContextDependencyTracker_1 = require("./ContextDependencyTracker");
|
|
47
|
+
const ErrorRecoveryManager_1 = require("./ErrorRecoveryManager");
|
|
48
|
+
const StructuralChangeDetector_1 = require("./StructuralChangeDetector");
|
|
49
|
+
const useInstance_1 = require("../hooks/useInstance");
|
|
50
|
+
const useState_1 = require("../hooks/useState");
|
|
51
|
+
const useContext_1 = require("../hooks/useContext");
|
|
52
|
+
const errors_1 = require("./errors");
|
|
53
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
54
|
+
// Color utilities for console output
|
|
55
|
+
const colors = {
|
|
56
|
+
success: chalk_1.default.green,
|
|
57
|
+
error: chalk_1.default.red,
|
|
58
|
+
warning: chalk_1.default.yellow,
|
|
59
|
+
info: chalk_1.default.blue,
|
|
60
|
+
dim: chalk_1.default.dim,
|
|
61
|
+
bold: chalk_1.default.bold,
|
|
62
|
+
checkmark: chalk_1.default.green('✓'),
|
|
63
|
+
cross: chalk_1.default.red('✗'),
|
|
64
|
+
bullet: chalk_1.default.gray('•'),
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Runtime class for CReact execution management
|
|
68
|
+
*
|
|
69
|
+
* REQ-1.1: Dedicated Runtime class for managing execution lifecycle
|
|
70
|
+
* REQ-1.2: Initialize renderer, reconciler, state machine, and providers
|
|
71
|
+
* REQ-1.3: Execute full deployment pipeline
|
|
72
|
+
* REQ-1.4: Compute deployment diff without executing
|
|
73
|
+
* REQ-1.5: Tear down resources in reverse dependency order
|
|
74
|
+
* REQ-1.6: Accept provider instances, backend configuration, and deployment options
|
|
75
|
+
* REQ-1.7: Multiple Runtime instances are isolated from each other
|
|
76
|
+
* REQ-1.8: Provide lifecycle hooks (onStart, onComplete, onError)
|
|
77
|
+
* REQ-1.9: Clean up all resources and connections
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const runtime = new Runtime({
|
|
82
|
+
* cloudProvider: new MyCloudProvider(),
|
|
83
|
+
* backendProvider: new MyBackendProvider(),
|
|
84
|
+
* deployment: {
|
|
85
|
+
* parallel: true,
|
|
86
|
+
* maxConcurrency: 10
|
|
87
|
+
* },
|
|
88
|
+
* log: {
|
|
89
|
+
* scopes: ['runtime', 'reconciler'],
|
|
90
|
+
* level: 'info'
|
|
91
|
+
* },
|
|
92
|
+
* hooks: {
|
|
93
|
+
* onStart: () => console.log('Deployment starting...'),
|
|
94
|
+
* onComplete: (result) => console.log('Deployment complete!'),
|
|
95
|
+
* onError: (error) => console.error('Deployment failed:', error)
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* // Deploy application
|
|
100
|
+
* const result = await runtime.deploy(<MyApp />);
|
|
101
|
+
*
|
|
102
|
+
* // Clean up
|
|
103
|
+
* await runtime.dispose();
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
class Runtime {
|
|
107
|
+
/**
|
|
108
|
+
* Constructor receives all dependencies via config (dependency injection)
|
|
109
|
+
* REQ-1.2: Initialize renderer, reconciler, state machine, and providers
|
|
110
|
+
* REQ-1.6: Accept configuration with providers and options
|
|
111
|
+
* REQ-1.7: Ensure multiple Runtime instances are isolated
|
|
112
|
+
*
|
|
113
|
+
* @param config - Runtime configuration with providers and options
|
|
114
|
+
*/
|
|
115
|
+
constructor(config) {
|
|
116
|
+
this.config = config;
|
|
117
|
+
// Instance state
|
|
118
|
+
this.lastFiberTree = null;
|
|
119
|
+
this.isDisposed = false;
|
|
120
|
+
console.log(colors.dim('Initializing Runtime...'));
|
|
121
|
+
// REQ-1.2: Initialize core components
|
|
122
|
+
this.renderer = new Renderer_1.Renderer();
|
|
123
|
+
this.validator = new Validator_1.Validator();
|
|
124
|
+
this.cloudDOMBuilder = new CloudDOMBuilder_1.CloudDOMBuilder(config.cloudProvider);
|
|
125
|
+
this.reconciler = new Reconciler_1.Reconciler();
|
|
126
|
+
this.stateMachine = new StateMachine_1.StateMachine(config.backendProvider);
|
|
127
|
+
// Initialize reactive system components
|
|
128
|
+
this.renderScheduler = new RenderScheduler_1.RenderScheduler();
|
|
129
|
+
this.stateBindingManager = new StateBindingManager_1.StateBindingManager();
|
|
130
|
+
this.providerOutputTracker = new ProviderOutputTracker_1.ProviderOutputTracker();
|
|
131
|
+
this.contextDependencyTracker = new ContextDependencyTracker_1.ContextDependencyTracker();
|
|
132
|
+
this.errorRecoveryManager = new ErrorRecoveryManager_1.ErrorRecoveryManager();
|
|
133
|
+
this.structuralChangeDetector = new StructuralChangeDetector_1.StructuralChangeDetector();
|
|
134
|
+
// Wire up reactive components
|
|
135
|
+
this.renderer.setRenderScheduler(this.renderScheduler);
|
|
136
|
+
this.renderer.setContextDependencyTracker(this.contextDependencyTracker);
|
|
137
|
+
this.renderer.setStructuralChangeDetector(this.structuralChangeDetector);
|
|
138
|
+
this.cloudDOMBuilder.setReactiveComponents(this.stateBindingManager, this.providerOutputTracker);
|
|
139
|
+
this.contextDependencyTracker.setStateBindingManager(this.stateBindingManager);
|
|
140
|
+
// Set global hook contexts (for this Runtime instance)
|
|
141
|
+
(0, useContext_1.setContextDependencyTracker)(this.contextDependencyTracker);
|
|
142
|
+
(0, useState_1.setStateBindingManager)(this.stateBindingManager);
|
|
143
|
+
(0, useInstance_1.setProviderOutputTracker)(this.providerOutputTracker);
|
|
144
|
+
// Subscribe to provider output change events (event-driven reactivity)
|
|
145
|
+
if (config.cloudProvider.on) {
|
|
146
|
+
config.cloudProvider.on('outputsChanged', (change) => {
|
|
147
|
+
this.handleProviderOutputChange(change).catch((error) => {
|
|
148
|
+
console.error(colors.error('Error handling provider output change:'), error);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
console.log(colors.dim('✓ Subscribed to provider output events'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Deploy the application
|
|
156
|
+
* REQ-1.3: Execute full deployment pipeline and return DeploymentResult
|
|
157
|
+
* REQ-1.8: Lifecycle hooks support (onStart, onComplete, onError)
|
|
158
|
+
*
|
|
159
|
+
* Pipeline: render → validate → build → reconcile → deploy
|
|
160
|
+
*
|
|
161
|
+
* @param app - JSX element to deploy
|
|
162
|
+
* @param stackName - Stack name for state management (default: 'default')
|
|
163
|
+
* @param user - User initiating deployment (default: 'system')
|
|
164
|
+
* @returns Promise resolving to DeploymentResult
|
|
165
|
+
*/
|
|
166
|
+
async deploy(app, stackName = 'default', user = 'system') {
|
|
167
|
+
this.ensureNotDisposed();
|
|
168
|
+
const startTime = Date.now();
|
|
169
|
+
try {
|
|
170
|
+
// REQ-1.8: Call onStart lifecycle hook
|
|
171
|
+
if (this.config.hooks?.onStart) {
|
|
172
|
+
await this.config.hooks.onStart();
|
|
173
|
+
}
|
|
174
|
+
// Step 1: Load previous state
|
|
175
|
+
const previousState = await this.stateMachine.getState(stackName);
|
|
176
|
+
// Step 2: Prepare hydration if we have previous state
|
|
177
|
+
if (previousState?.cloudDOM) {
|
|
178
|
+
this.prepareHydration(previousState.cloudDOM);
|
|
179
|
+
(0, useInstance_1.setPreviousOutputs)(this.buildOutputsMap(previousState.cloudDOM));
|
|
180
|
+
}
|
|
181
|
+
// Step 3: Render JSX → Fiber
|
|
182
|
+
const fiber = this.renderer.render(app);
|
|
183
|
+
this.lastFiberTree = fiber;
|
|
184
|
+
// Step 4: Clear hydration and previous outputs after render
|
|
185
|
+
this.clearHydration();
|
|
186
|
+
(0, useInstance_1.setPreviousOutputs)(null);
|
|
187
|
+
// Step 5: Validate Fiber
|
|
188
|
+
this.validator.validate(fiber);
|
|
189
|
+
// Step 6: Build CloudDOM from Fiber
|
|
190
|
+
const cloudDOM = await this.cloudDOMBuilder.build(fiber);
|
|
191
|
+
// Step 7: Compute diff
|
|
192
|
+
const previousCloudDOM = previousState?.cloudDOM || [];
|
|
193
|
+
const changeSet = this.reconciler.reconcile(previousCloudDOM, cloudDOM);
|
|
194
|
+
// Check if there are any changes
|
|
195
|
+
if (!(0, Reconciler_1.hasChanges)(changeSet)) {
|
|
196
|
+
console.log(colors.dim('No changes.'));
|
|
197
|
+
const duration = Date.now() - startTime;
|
|
198
|
+
const result = {
|
|
199
|
+
cloudDOM,
|
|
200
|
+
changeSet,
|
|
201
|
+
duration,
|
|
202
|
+
outputs: this.extractOutputs(cloudDOM),
|
|
203
|
+
stats: {
|
|
204
|
+
totalResources: cloudDOM.length,
|
|
205
|
+
created: 0,
|
|
206
|
+
updated: 0,
|
|
207
|
+
deleted: 0,
|
|
208
|
+
parallelBatches: 0,
|
|
209
|
+
parallelEfficiency: 0,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
// REQ-1.8: Call onComplete lifecycle hook
|
|
213
|
+
if (this.config.hooks?.onComplete) {
|
|
214
|
+
await this.config.hooks.onComplete(result);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
// Display changes summary with diff syntax
|
|
219
|
+
const total = changeSet.creates.length + changeSet.updates.length + changeSet.deletes.length;
|
|
220
|
+
console.log(colors.dim(`\n${total} change${total !== 1 ? 's' : ''}\n`));
|
|
221
|
+
// Show detailed diff
|
|
222
|
+
if (changeSet.creates.length > 0) {
|
|
223
|
+
changeSet.creates.forEach((node) => {
|
|
224
|
+
const resourceType = node.constructType || node.construct?.name || 'Resource';
|
|
225
|
+
console.log(colors.success(` + ${resourceType}`), colors.dim(node.id));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (changeSet.updates.length > 0) {
|
|
229
|
+
changeSet.updates.forEach((node) => {
|
|
230
|
+
const resourceType = node.constructType || node.construct?.name || 'Resource';
|
|
231
|
+
console.log(colors.warning(` ~ ${resourceType}`), colors.dim(node.id));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (changeSet.deletes.length > 0) {
|
|
235
|
+
changeSet.deletes.forEach((node) => {
|
|
236
|
+
const resourceType = node.constructType || node.construct?.name || 'Resource';
|
|
237
|
+
console.log(colors.error(` - ${resourceType}`), colors.dim(node.id));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
console.log(); // Empty line before deployment starts
|
|
241
|
+
// Step 8: Start deployment via StateMachine
|
|
242
|
+
await this.stateMachine.startDeployment(stackName, changeSet, cloudDOM, user);
|
|
243
|
+
// Step 9: Call preDeploy lifecycle hook
|
|
244
|
+
if (this.config.cloudProvider.preDeploy) {
|
|
245
|
+
await this.config.cloudProvider.preDeploy(cloudDOM);
|
|
246
|
+
}
|
|
247
|
+
// Step 10: Deploy resources in order
|
|
248
|
+
for (let i = 0; i < changeSet.deploymentOrder.length; i++) {
|
|
249
|
+
const resourceId = changeSet.deploymentOrder[i];
|
|
250
|
+
const resourceNode = this.findNodeById(cloudDOM, resourceId);
|
|
251
|
+
if (!resourceNode) {
|
|
252
|
+
throw new errors_1.DeploymentError(`Resource not found: ${resourceId}`, {
|
|
253
|
+
message: `Resource not found: ${resourceId}`,
|
|
254
|
+
code: 'RESOURCE_NOT_FOUND',
|
|
255
|
+
details: { resourceId, stackName },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Materialize single resource
|
|
259
|
+
await this.config.cloudProvider.materialize([resourceNode], null);
|
|
260
|
+
// Update checkpoint
|
|
261
|
+
await this.stateMachine.updateCheckpoint(stackName, i);
|
|
262
|
+
}
|
|
263
|
+
// Step 11: Collect outputs
|
|
264
|
+
const outputs = this.extractOutputs(cloudDOM);
|
|
265
|
+
// Step 12: Call postDeploy lifecycle hook
|
|
266
|
+
if (this.config.cloudProvider.postDeploy) {
|
|
267
|
+
await this.config.cloudProvider.postDeploy(cloudDOM, outputs);
|
|
268
|
+
}
|
|
269
|
+
// Step 13: Complete deployment
|
|
270
|
+
await this.stateMachine.completeDeployment(stackName);
|
|
271
|
+
const duration = Date.now() - startTime;
|
|
272
|
+
// Calculate statistics
|
|
273
|
+
const stats = {
|
|
274
|
+
totalResources: cloudDOM.length,
|
|
275
|
+
created: changeSet.creates.length,
|
|
276
|
+
updated: changeSet.updates.length,
|
|
277
|
+
deleted: changeSet.deletes.length,
|
|
278
|
+
parallelBatches: changeSet.parallelBatches.length,
|
|
279
|
+
parallelEfficiency: this.calculateParallelEfficiency(changeSet),
|
|
280
|
+
};
|
|
281
|
+
console.log(colors.success(`\n✓ Deployed in ${(duration / 1000).toFixed(2)}s`));
|
|
282
|
+
const result = {
|
|
283
|
+
cloudDOM,
|
|
284
|
+
changeSet,
|
|
285
|
+
duration,
|
|
286
|
+
outputs,
|
|
287
|
+
stats,
|
|
288
|
+
};
|
|
289
|
+
// REQ-1.8: Call onComplete lifecycle hook
|
|
290
|
+
if (this.config.hooks?.onComplete) {
|
|
291
|
+
await this.config.hooks.onComplete(result);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
console.error(colors.error(`\n✗ ${error.message}`));
|
|
297
|
+
// NEW (Gap 3): Attempt error recovery before failing
|
|
298
|
+
if (this.lastFiberTree) {
|
|
299
|
+
try {
|
|
300
|
+
console.log(colors.info('Attempting error recovery...'));
|
|
301
|
+
// Create snapshot for potential rollback
|
|
302
|
+
this.errorRecoveryManager.createComponentSnapshot(this.lastFiberTree);
|
|
303
|
+
// Attempt recovery using re-render error handler
|
|
304
|
+
const recoveryResult = await this.errorRecoveryManager.handleReRenderError(error, [this.lastFiberTree], 'manual');
|
|
305
|
+
if (recoveryResult.success) {
|
|
306
|
+
console.log(colors.success(`✓ Error recovery succeeded: ${recoveryResult.message}`));
|
|
307
|
+
// Recovery succeeded - return partial result
|
|
308
|
+
const duration = Date.now() - startTime;
|
|
309
|
+
return {
|
|
310
|
+
cloudDOM: recoveryResult.recoveredFibers?.[0]?.cloudDOMNodes || [],
|
|
311
|
+
changeSet: {
|
|
312
|
+
creates: [],
|
|
313
|
+
updates: [],
|
|
314
|
+
deletes: [],
|
|
315
|
+
replacements: [],
|
|
316
|
+
moves: [],
|
|
317
|
+
deploymentOrder: [],
|
|
318
|
+
parallelBatches: [],
|
|
319
|
+
},
|
|
320
|
+
duration,
|
|
321
|
+
outputs: {},
|
|
322
|
+
stats: {
|
|
323
|
+
totalResources: 0,
|
|
324
|
+
created: 0,
|
|
325
|
+
updated: 0,
|
|
326
|
+
deleted: 0,
|
|
327
|
+
parallelBatches: 0,
|
|
328
|
+
parallelEfficiency: 0,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
console.log(colors.warning(`Error recovery failed: ${recoveryResult.message}`));
|
|
333
|
+
}
|
|
334
|
+
catch (recoveryError) {
|
|
335
|
+
console.error(colors.error('Error during recovery attempt:'), recoveryError);
|
|
336
|
+
// Continue with normal error handling
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// REQ-1.8: Call onError lifecycle hook
|
|
340
|
+
if (this.config.hooks?.onError) {
|
|
341
|
+
await this.config.hooks.onError(error);
|
|
342
|
+
}
|
|
343
|
+
// Call provider onError hook
|
|
344
|
+
if (this.config.cloudProvider.onError) {
|
|
345
|
+
const cloudDOM = await this.getState(stackName);
|
|
346
|
+
await this.config.cloudProvider.onError(error, cloudDOM);
|
|
347
|
+
}
|
|
348
|
+
// Mark deployment as failed
|
|
349
|
+
const deploymentError = error instanceof errors_1.DeploymentError
|
|
350
|
+
? error
|
|
351
|
+
: new errors_1.DeploymentError(error.message, {
|
|
352
|
+
message: error.message,
|
|
353
|
+
code: 'DEPLOYMENT_FAILED',
|
|
354
|
+
stack: error.stack,
|
|
355
|
+
});
|
|
356
|
+
await this.stateMachine.failDeployment(stackName, deploymentError);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Compute deployment plan without executing
|
|
362
|
+
* REQ-1.4: Plan method computes ChangeSet without executing
|
|
363
|
+
*
|
|
364
|
+
* @param app - JSX element to plan
|
|
365
|
+
* @param stackName - Stack name for state comparison (default: 'default')
|
|
366
|
+
* @returns Promise resolving to ChangeSet
|
|
367
|
+
*/
|
|
368
|
+
async plan(app, stackName = 'default') {
|
|
369
|
+
this.ensureNotDisposed();
|
|
370
|
+
try {
|
|
371
|
+
// Step 1: Load previous state
|
|
372
|
+
const previousState = await this.stateMachine.getState(stackName);
|
|
373
|
+
// Step 2: Prepare hydration if we have previous state
|
|
374
|
+
if (previousState?.cloudDOM) {
|
|
375
|
+
this.prepareHydration(previousState.cloudDOM);
|
|
376
|
+
(0, useInstance_1.setPreviousOutputs)(this.buildOutputsMap(previousState.cloudDOM));
|
|
377
|
+
}
|
|
378
|
+
// Step 3: Render JSX → Fiber
|
|
379
|
+
const fiber = this.renderer.render(app);
|
|
380
|
+
// Step 4: Clear hydration
|
|
381
|
+
this.clearHydration();
|
|
382
|
+
(0, useInstance_1.setPreviousOutputs)(null);
|
|
383
|
+
// Step 5: Validate Fiber
|
|
384
|
+
this.validator.validate(fiber);
|
|
385
|
+
// Step 6: Build CloudDOM
|
|
386
|
+
const cloudDOM = await this.cloudDOMBuilder.build(fiber);
|
|
387
|
+
// Step 7: Compute diff
|
|
388
|
+
const previousCloudDOM = previousState?.cloudDOM || [];
|
|
389
|
+
const changeSet = this.reconciler.reconcile(previousCloudDOM, cloudDOM);
|
|
390
|
+
const total = changeSet.creates.length + changeSet.updates.length + changeSet.deletes.length;
|
|
391
|
+
if (total === 0) {
|
|
392
|
+
console.log(colors.dim('No changes.'));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
console.log(colors.dim(`${total} change${total !== 1 ? 's' : ''}\n`));
|
|
396
|
+
// Show detailed diff
|
|
397
|
+
if (changeSet.creates.length > 0) {
|
|
398
|
+
changeSet.creates.forEach((node) => {
|
|
399
|
+
const resourceType = node.constructType || node.construct?.name || 'Resource';
|
|
400
|
+
console.log(colors.success(` + ${resourceType}`), colors.dim(node.id));
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (changeSet.updates.length > 0) {
|
|
404
|
+
changeSet.updates.forEach((node) => {
|
|
405
|
+
const resourceType = node.constructType || node.construct?.name || 'Resource';
|
|
406
|
+
console.log(colors.warning(` ~ ${resourceType}`), colors.dim(node.id));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (changeSet.deletes.length > 0) {
|
|
410
|
+
changeSet.deletes.forEach((node) => {
|
|
411
|
+
const resourceType = node.constructType || node.construct?.name || 'Resource';
|
|
412
|
+
console.log(colors.error(` - ${resourceType}`), colors.dim(node.id));
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return changeSet;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
console.error(colors.error(`✗ ${error.message}`));
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Destroy all resources
|
|
425
|
+
* REQ-1.5: Tear down resources in reverse dependency order
|
|
426
|
+
*
|
|
427
|
+
* @param stackName - Stack name to destroy (default: 'default')
|
|
428
|
+
*/
|
|
429
|
+
async destroy(stackName = 'default') {
|
|
430
|
+
this.ensureNotDisposed();
|
|
431
|
+
try {
|
|
432
|
+
// Get current state
|
|
433
|
+
const state = await this.stateMachine.getState(stackName);
|
|
434
|
+
if (!state || !state.cloudDOM || state.cloudDOM.length === 0) {
|
|
435
|
+
console.log(colors.dim('Nothing to destroy.'));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Compute reverse deployment order
|
|
439
|
+
const changeSet = this.reconciler.reconcile(state.cloudDOM, []);
|
|
440
|
+
const reverseOrder = [...changeSet.deploymentOrder].reverse();
|
|
441
|
+
console.log(colors.dim(`${reverseOrder.length} resource${reverseOrder.length !== 1 ? 's' : ''} to destroy\n`));
|
|
442
|
+
// Show what will be destroyed
|
|
443
|
+
for (let i = 0; i < reverseOrder.length; i++) {
|
|
444
|
+
const resourceId = reverseOrder[i];
|
|
445
|
+
const resourceNode = this.findNodeById(state.cloudDOM, resourceId);
|
|
446
|
+
if (resourceNode) {
|
|
447
|
+
const resourceType = resourceNode.constructType || resourceNode.construct?.name || 'Resource';
|
|
448
|
+
console.log(colors.error(` - ${resourceType}`), colors.dim(resourceNode.id));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
console.log(); // Empty line
|
|
452
|
+
// Delete resources in reverse order
|
|
453
|
+
for (let i = 0; i < reverseOrder.length; i++) {
|
|
454
|
+
const resourceId = reverseOrder[i];
|
|
455
|
+
const resourceNode = this.findNodeById(state.cloudDOM, resourceId);
|
|
456
|
+
if (resourceNode) {
|
|
457
|
+
// Note: Actual deletion would be handled by provider
|
|
458
|
+
// For now, we just log the action
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Clear state by saving empty CloudDOM
|
|
462
|
+
await this.config.backendProvider.saveState(stackName, {
|
|
463
|
+
status: 'DEPLOYED',
|
|
464
|
+
cloudDOM: [],
|
|
465
|
+
timestamp: Date.now(),
|
|
466
|
+
user: 'system',
|
|
467
|
+
stackName,
|
|
468
|
+
});
|
|
469
|
+
console.log(colors.success('✓ Destroyed'));
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
console.error(colors.error(`✗ ${error.message}`));
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Get current CloudDOM state
|
|
478
|
+
* REQ-1.6: Get current CloudDOM state
|
|
479
|
+
*
|
|
480
|
+
* @param stackName - Stack name (default: 'default')
|
|
481
|
+
* @returns Promise resolving to CloudDOM tree
|
|
482
|
+
*/
|
|
483
|
+
async getState(stackName = 'default') {
|
|
484
|
+
this.ensureNotDisposed();
|
|
485
|
+
const state = await this.stateMachine.getState(stackName);
|
|
486
|
+
return state?.cloudDOM || [];
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Dispose runtime and clean up resources
|
|
490
|
+
* REQ-1.9: Clean up all resources and connections
|
|
491
|
+
*/
|
|
492
|
+
async dispose() {
|
|
493
|
+
if (this.isDisposed) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Clear hook contexts
|
|
497
|
+
(0, useContext_1.setContextDependencyTracker)(null);
|
|
498
|
+
(0, useState_1.setStateBindingManager)(null);
|
|
499
|
+
(0, useInstance_1.setProviderOutputTracker)(null);
|
|
500
|
+
(0, useInstance_1.setPreviousOutputs)(null);
|
|
501
|
+
// Mark as disposed
|
|
502
|
+
this.isDisposed = true;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Ensure runtime is not disposed
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
ensureNotDisposed() {
|
|
509
|
+
if (this.isDisposed) {
|
|
510
|
+
throw new Error('Runtime has been disposed and cannot be used');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Handle output change events from provider (event-driven reactivity)
|
|
515
|
+
* Called when provider emits 'outputsChanged' event
|
|
516
|
+
*
|
|
517
|
+
* This enables real-time reactivity without polling:
|
|
518
|
+
* 1. Update ProviderOutputTracker with new outputs
|
|
519
|
+
* 2. Execute useEffect callbacks bound to these outputs (TODO)
|
|
520
|
+
* 3. Update bound state and enqueue affected fibers for re-render
|
|
521
|
+
*
|
|
522
|
+
* @param change - Output change event from provider
|
|
523
|
+
* @private
|
|
524
|
+
*/
|
|
525
|
+
async handleProviderOutputChange(change) {
|
|
526
|
+
if (!this.lastFiberTree) {
|
|
527
|
+
return; // No fiber tree to update
|
|
528
|
+
}
|
|
529
|
+
console.log(colors.dim(`Provider output changed: ${change.nodeId}`), change.outputs);
|
|
530
|
+
// Step 1: Update ProviderOutputTracker
|
|
531
|
+
const outputChanges = this.providerOutputTracker.updateInstanceOutputs(change.nodeId, change.outputs);
|
|
532
|
+
if (outputChanges.length === 0) {
|
|
533
|
+
return; // No actual changes detected
|
|
534
|
+
}
|
|
535
|
+
// Step 2: Execute useEffect callbacks bound to these outputs
|
|
536
|
+
// TODO: Implement executeEffectsOnOutputChange when useEffect is ready
|
|
537
|
+
// await executeEffectsOnOutputChange(this.lastFiberTree, outputChanges);
|
|
538
|
+
// Step 3: Update bound state and get affected fibers
|
|
539
|
+
const affectedFibers = this.stateBindingManager.processOutputChanges(outputChanges);
|
|
540
|
+
if (affectedFibers.length > 0) {
|
|
541
|
+
console.log(colors.info(`✓ Output change affected ${affectedFibers.length} fibers`));
|
|
542
|
+
// Schedule re-renders for affected components
|
|
543
|
+
affectedFibers.forEach((fiber) => {
|
|
544
|
+
this.renderScheduler.schedule(fiber, 'output-update');
|
|
545
|
+
});
|
|
546
|
+
// Note: Actual re-render execution happens in the next deployment cycle
|
|
547
|
+
// or can be triggered immediately via renderScheduler.flushBatch()
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Prepare hydration data from previous CloudDOM
|
|
552
|
+
* @private
|
|
553
|
+
*/
|
|
554
|
+
prepareHydration(previousCloudDOM) {
|
|
555
|
+
// This would integrate with the hydration system
|
|
556
|
+
// For now, we'll keep it simple
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Clear hydration data
|
|
560
|
+
* @private
|
|
561
|
+
*/
|
|
562
|
+
clearHydration() {
|
|
563
|
+
// Clear hydration
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Build outputs map from CloudDOM
|
|
567
|
+
* @private
|
|
568
|
+
*/
|
|
569
|
+
buildOutputsMap(cloudDOM) {
|
|
570
|
+
const outputsMap = new Map();
|
|
571
|
+
const walk = (nodes) => {
|
|
572
|
+
for (const node of nodes) {
|
|
573
|
+
if (node.outputs && Object.keys(node.outputs).length > 0) {
|
|
574
|
+
outputsMap.set(node.id, node.outputs);
|
|
575
|
+
}
|
|
576
|
+
if (node.children && node.children.length > 0) {
|
|
577
|
+
walk(node.children);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
walk(cloudDOM);
|
|
582
|
+
return outputsMap;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Find node by ID in CloudDOM tree
|
|
586
|
+
* @private
|
|
587
|
+
*/
|
|
588
|
+
findNodeById(cloudDOM, id) {
|
|
589
|
+
for (const node of cloudDOM) {
|
|
590
|
+
if (node.id === id) {
|
|
591
|
+
return node;
|
|
592
|
+
}
|
|
593
|
+
if (node.children && node.children.length > 0) {
|
|
594
|
+
const found = this.findNodeById(node.children, id);
|
|
595
|
+
if (found) {
|
|
596
|
+
return found;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Extract outputs from CloudDOM
|
|
604
|
+
* @private
|
|
605
|
+
*/
|
|
606
|
+
extractOutputs(cloudDOM) {
|
|
607
|
+
const outputs = {};
|
|
608
|
+
const walk = (nodes) => {
|
|
609
|
+
for (const node of nodes) {
|
|
610
|
+
if (node.outputs) {
|
|
611
|
+
outputs[node.id] = node.outputs;
|
|
612
|
+
}
|
|
613
|
+
if (node.children && node.children.length > 0) {
|
|
614
|
+
walk(node.children);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
walk(cloudDOM);
|
|
619
|
+
return outputs;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Calculate parallel efficiency from change set
|
|
623
|
+
* @private
|
|
624
|
+
*/
|
|
625
|
+
calculateParallelEfficiency(changeSet) {
|
|
626
|
+
if (changeSet.parallelBatches.length === 0) {
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
const totalResources = changeSet.deploymentOrder.length;
|
|
630
|
+
const batchCount = changeSet.parallelBatches.length;
|
|
631
|
+
// Perfect parallelism would be all resources in one batch
|
|
632
|
+
// No parallelism would be one resource per batch
|
|
633
|
+
// Efficiency = (totalResources - batchCount) / (totalResources - 1)
|
|
634
|
+
if (totalResources <= 1) {
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
return (totalResources - batchCount) / (totalResources - 1);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
exports.Runtime = Runtime;
|