@creact-labs/creact 0.1.5 → 0.1.7
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/README.md +1 -348
- package/dist/cli/commands/DevCommand.js +11 -2
- package/dist/cli/index.js +0 -0
- package/dist/core/CReact.js +19 -0
- package/dist/core/Reconciler.js +7 -0
- package/dist/core/StateMachine.d.ts +17 -0
- package/dist/core/StateMachine.js +96 -0
- package/dist/hooks/useInstance.js +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/providers/ICloudProvider.d.ts +84 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,12 +3,8 @@ NOTE: !!! THIS IS A EXPERIMENT OF THOUGHT NOT A PRODUCTION READY PRODUCT !!!
|
|
|
3
3
|
|
|
4
4
|
# CReact
|
|
5
5
|
|
|
6
|
-

|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
Think of it as React's rendering model — but instead of a DOM, you render to the cloud.
|
|
10
|
-
|
|
11
|
-
Write your cloud architecture in JSX. CReact figures out the dependencies, orchestrates the deployment, and keeps everything in sync.
|
|
7
|
+
Write your cloud architecture in JSX
|
|
12
8
|
|
|
13
9
|
```tsx
|
|
14
10
|
function App() {
|
|
@@ -24,351 +20,8 @@ function App() {
|
|
|
24
20
|
}
|
|
25
21
|
```
|
|
26
22
|
|
|
27
|
-
That's it. No dependency graphs, no explicit ordering, no YAML. Just components that render to actual infrastructure.
|
|
28
|
-
|
|
29
|
-
## The Problem
|
|
30
|
-
|
|
31
|
-
Terraform and Pulumi are great, but they're static. You write a plan, run it, and hope nothing breaks. If you need dynamic orchestration - like deploying a database, waiting for its endpoint, then using that endpoint in your API config - you're writing bash scripts or custom tooling.
|
|
32
|
-
|
|
33
|
-
CReact treats infrastructure like React treats UI. Components re-render when their dependencies change. Outputs flow naturally through context. The Reconciler figures out what actually needs to deploy.
|
|
34
|
-
|
|
35
|
-
## What Makes It Different
|
|
36
|
-
|
|
37
|
-
**It's reactive** - when a database finishes deploying and outputs its endpoint, components that depend on it automatically re-render and deploy. No manual dependency chains.
|
|
38
|
-
|
|
39
|
-
**It's a compiler** - your JSX compiles to CloudDOM (basically an AST for infrastructure). You can diff it, version it, test it without cloud credentials. Only when you're ready does it materialize to real resources.
|
|
40
|
-
|
|
41
|
-
**It's resilient** - every deployment is checkpointed and resumable. Crash halfway through? Resume from where you left off. The Reconciler only deploys what changed, like React's virtual DOM but for cloud resources.
|
|
42
|
-
|
|
43
|
-
**It works with existing tools** - CReact doesn't replace Terraform or CDK. It orchestrates them. Wrap your Terraform modules as CReact components and compose them together.
|
|
44
|
-
|
|
45
23
|
## Watch It Work
|
|
46
24
|
|
|
47
|
-
Here's a real deployment - 22 resources across 3 regions, 6 reactive cycles, fully automatic:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
$ creact dev --entry reactive-app.tsx --auto-approve
|
|
51
|
-
|
|
52
|
-
Starting CReact development mode...
|
|
53
|
-
Building initial state...
|
|
54
|
-
Changes detected: 2 changes
|
|
55
|
-
|
|
56
|
-
Planning changes for stack: optimized-reactive-app-stack
|
|
57
|
-
──────────────────────────────────────────────────
|
|
58
|
-
+ 2 to create
|
|
59
|
-
+ Analytics (Kinesis stream for API analytics)
|
|
60
|
-
+ VPC (Network foundation)
|
|
61
|
-
|
|
62
|
-
Plan: 2 to add, 0 to change, 0 to destroy
|
|
63
|
-
Auto-approving changes...
|
|
64
|
-
|
|
65
|
-
Applying changes...
|
|
66
|
-
[MockCloud] ✅ All resources materialized
|
|
67
|
-
Apply complete! Resources: 2 added, 0 changed, 0 destroyed
|
|
68
|
-
Duration: 0.3s
|
|
69
|
-
|
|
70
|
-
Reactive changes detected
|
|
71
|
-
+ 6 to create
|
|
72
|
-
+ S3Bucket (Asset storage)
|
|
73
|
-
+ SecurityGroup (Network security)
|
|
74
|
-
+ Subnet × 4 (Multi-AZ subnets)
|
|
75
|
-
|
|
76
|
-
Plan: 6 to add, 0 to change, 0 to destroy
|
|
77
|
-
Reactive deployment cycle #2
|
|
78
|
-
Apply complete! Resources: 6 added, 0 changed, 0 destroyed
|
|
79
|
-
Duration: 0.1s
|
|
80
|
-
|
|
81
|
-
Reactive changes detected
|
|
82
|
-
+ 4 to create
|
|
83
|
-
+ ElastiCache (Redis cluster)
|
|
84
|
-
+ CloudFront (CDN distribution)
|
|
85
|
-
+ LoadBalancer (Application load balancer)
|
|
86
|
-
+ RDSInstance (PostgreSQL database)
|
|
87
|
-
|
|
88
|
-
Reactive deployment cycle #3
|
|
89
|
-
Apply complete! Resources: 4 added, 0 changed, 0 destroyed
|
|
90
|
-
Duration: 0.5s
|
|
91
|
-
|
|
92
|
-
Reactive changes detected
|
|
93
|
-
+ 4 to create
|
|
94
|
-
+ Lambda × 3 (Regional API handlers: us-east-1, eu-west-1, ap-southeast-1)
|
|
95
|
-
+ Backup (Database backup vault)
|
|
96
|
-
|
|
97
|
-
Reactive deployment cycle #4
|
|
98
|
-
Apply complete! Resources: 4 added, 0 changed, 0 destroyed
|
|
99
|
-
Duration: 0.5s
|
|
100
|
-
|
|
101
|
-
Reactive changes detected
|
|
102
|
-
+ 3 to create
|
|
103
|
-
+ ApiGateway × 3 (Regional API endpoints)
|
|
104
|
-
|
|
105
|
-
Reactive deployment cycle #5
|
|
106
|
-
Apply complete! Resources: 3 added, 0 changed, 0 destroyed
|
|
107
|
-
Duration: 0.4s
|
|
108
|
-
|
|
109
|
-
Reactive changes detected
|
|
110
|
-
+ 3 to create
|
|
111
|
-
+ CloudWatch × 3 (Regional monitoring dashboards)
|
|
112
|
-
|
|
113
|
-
Reactive deployment cycle #6
|
|
114
|
-
Apply complete! Resources: 3 added, 0 changed, 0 destroyed
|
|
115
|
-
Duration: 0.4s
|
|
116
|
-
|
|
117
|
-
✅ Deployment complete: 22 resources across 6 reactive cycles
|
|
118
|
-
Watching for changes... (Press Ctrl+C to stop)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
**What happened:**
|
|
122
|
-
|
|
123
|
-
VPC deployed first. When its outputs became available, subnets and security groups deployed in parallel. When those finished, database and cache deployed. When the database endpoint was ready, Lambdas deployed. When Lambdas got function ARNs, API Gateways deployed. When APIs were live, monitoring deployed.
|
|
124
|
-
|
|
125
|
-
You didn't orchestrate anything manually. You just wrote JSX components that reference each other's outputs. CReact figured out the rest.
|
|
126
|
-
|
|
127
|
-
## How It Works
|
|
128
|
-
|
|
129
|
-
### JSX Compiles to CloudDOM
|
|
130
|
-
|
|
131
|
-
```tsx
|
|
132
|
-
<VPC name="app-vpc" cidr="10.0.0.0/16" />
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
becomes:
|
|
136
|
-
|
|
137
|
-
```json
|
|
138
|
-
{
|
|
139
|
-
"id": "app-vpc",
|
|
140
|
-
"construct": "VPC",
|
|
141
|
-
"props": { "cidr": "10.0.0.0/16" },
|
|
142
|
-
"outputs": {}
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
CloudDOM is just a JSON tree. You can diff it like Git, version it, test it without touching AWS. It's the intermediate representation between your code and actual cloud resources.
|
|
147
|
-
|
|
148
|
-
### The Reconciler Figures Out What Changed
|
|
149
|
-
|
|
150
|
-
Before deploying anything, CReact diffs the previous CloudDOM against the new one. Creates, updates, deletes, replacements - all computed ahead of time. You get a Terraform-style plan but it's just comparing two data structures.
|
|
151
|
-
|
|
152
|
-
### Deployment Is Checkpointed
|
|
153
|
-
|
|
154
|
-
After each resource deploys, CReact saves a checkpoint. Crash halfway through? Run it again and it resumes from where it left off. No manual cleanup, no orphaned resources.
|
|
155
|
-
|
|
156
|
-
### Outputs Trigger Re-renders
|
|
157
|
-
|
|
158
|
-
When a resource finishes deploying, its outputs (endpoint, ARN, ID) become available. Any component that depends on those outputs automatically re-renders and creates new resources. It's like useEffect but for infrastructure.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
## Hooks
|
|
163
|
-
|
|
164
|
-
### useInstance - Create Resources
|
|
165
|
-
|
|
166
|
-
Creates cloud resources and provides access to their outputs:
|
|
167
|
-
|
|
168
|
-
```tsx
|
|
169
|
-
function DatabaseComponent() {
|
|
170
|
-
// Create database resource
|
|
171
|
-
const database = useInstance(Database, {
|
|
172
|
-
name: 'my-db',
|
|
173
|
-
engine: 'postgres'
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Access outputs after deployment
|
|
177
|
-
console.log(database.outputs?.endpoint); // "db.example.com:5432"
|
|
178
|
-
|
|
179
|
-
return <></>;
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
**Key Behavior**: If any prop value is `undefined`, creates a placeholder node that doesn't deploy. When dependencies become available, component re-renders and creates the real resource.
|
|
184
|
-
|
|
185
|
-
### useState - Persistent State
|
|
186
|
-
|
|
187
|
-
Manages state that persists across deployments:
|
|
188
|
-
|
|
189
|
-
```tsx
|
|
190
|
-
function App() {
|
|
191
|
-
// State survives deployments (stored in backend)
|
|
192
|
-
const [deployCount, setDeployCount] = useState(0);
|
|
193
|
-
const [appVersion] = useState('1.0.0');
|
|
194
|
-
|
|
195
|
-
// Update state (takes effect next deployment)
|
|
196
|
-
setDeployCount(prev => (prev || 0) + 1);
|
|
197
|
-
|
|
198
|
-
return <></>;
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**Advanced**: Can bind to provider outputs for reactivity:
|
|
203
|
-
|
|
204
|
-
```tsx
|
|
205
|
-
function DatabaseComponent() {
|
|
206
|
-
const db = useInstance(Database, { name: 'my-db' });
|
|
207
|
-
|
|
208
|
-
// This triggers re-renders when db.outputs.endpoint changes
|
|
209
|
-
const [endpoint, setEndpoint] = useState(db.outputs?.endpoint);
|
|
210
|
-
|
|
211
|
-
return <></>;
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### useContext - Share Dependencies
|
|
216
|
-
|
|
217
|
-
Shares data between components with reactivity when containing outputs:
|
|
218
|
-
|
|
219
|
-
```tsx
|
|
220
|
-
const DatabaseContext = createContext<{ endpoint?: string }>({});
|
|
221
|
-
|
|
222
|
-
function DatabaseProvider({ children }) {
|
|
223
|
-
const db = useInstance(Database, { name: 'my-db' });
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<DatabaseContext.Provider value={{
|
|
227
|
-
endpoint: db.outputs?.endpoint // Makes context reactive
|
|
228
|
-
}}>
|
|
229
|
-
{children}
|
|
230
|
-
</DatabaseContext.Provider>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function ApiConsumer() {
|
|
235
|
-
const { endpoint } = useContext(DatabaseContext); // Re-renders when endpoint available
|
|
236
|
-
|
|
237
|
-
const api = useInstance(ApiGateway, {
|
|
238
|
-
dbUrl: endpoint // Triggers re-render when endpoint changes
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
return <></>;
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Reactivity Mechanisms
|
|
246
|
-
|
|
247
|
-
CReact implements multiple interconnected reactivity systems:
|
|
248
|
-
|
|
249
|
-
1. **useState Binding**: `useState` can bind to provider outputs, triggering re-renders when outputs change
|
|
250
|
-
2. **Context Dependency Tracking**: Contexts containing outputs trigger re-renders in consuming components
|
|
251
|
-
3. **Output Change Detection**: Post-deployment effect system detects when provider outputs change
|
|
252
|
-
4. **Selective Re-rendering**: Only components affected by changes re-render
|
|
253
|
-
|
|
254
|
-
## Deployment Orchestration
|
|
255
|
-
|
|
256
|
-
### Reactive Deployment Cycles
|
|
257
|
-
|
|
258
|
-
```tsx
|
|
259
|
-
function MultiLayerApp() {
|
|
260
|
-
return (
|
|
261
|
-
<NetworkStack> {/* Layer 1: Deploy VPC first */}
|
|
262
|
-
<DatabaseStack> {/* Layer 2: Deploy DB/cache when VPC ready */}
|
|
263
|
-
<StorageStack> {/* Layer 3: Deploy S3/CDN when network ready */}
|
|
264
|
-
<ApiStack> {/* Layer 4: Deploy APIs when all deps ready */}
|
|
265
|
-
<MonitoringStack /> {/* Layer 5: Deploy monitoring last */}
|
|
266
|
-
</ApiStack>
|
|
267
|
-
</StorageStack>
|
|
268
|
-
</DatabaseStack>
|
|
269
|
-
</NetworkStack>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
**Deployment Flow**:
|
|
275
|
-
1. **Initial Render**: All components create resource references
|
|
276
|
-
2. **Wave 1**: Network resources deploy (no dependencies)
|
|
277
|
-
3. **Wave 2**: Data resources deploy (depend on network outputs)
|
|
278
|
-
4. **Wave 3**: Storage resources deploy (depend on network outputs)
|
|
279
|
-
5. **Wave 4**: API resources deploy (depend on data + storage outputs)
|
|
280
|
-
6. **Wave 5**: Monitoring resources deploy (depend on API outputs)
|
|
281
|
-
|
|
282
|
-
## Provider Implementation
|
|
283
|
-
|
|
284
|
-
### Cloud Provider Example
|
|
285
|
-
|
|
286
|
-
```tsx
|
|
287
|
-
class AWSProvider implements ICloudProvider {
|
|
288
|
-
async materialize(resources: CloudDOMNode[]): Promise<void> {
|
|
289
|
-
for (const resource of resources) {
|
|
290
|
-
if (resource.construct?.name === 'Database') {
|
|
291
|
-
const db = await this.createRDSInstance(resource.props);
|
|
292
|
-
resource.outputs = {
|
|
293
|
-
endpoint: db.endpoint,
|
|
294
|
-
connectionUrl: db.connectionString
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
// Handle other resource types...
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Backend Provider Example
|
|
304
|
-
|
|
305
|
-
```tsx
|
|
306
|
-
class S3BackendProvider implements IBackendProvider {
|
|
307
|
-
async getState(stackName: string): Promise<any> {
|
|
308
|
-
const state = await this.s3.getObject({
|
|
309
|
-
Bucket: 'my-state-bucket',
|
|
310
|
-
Key: `${stackName}.json`
|
|
311
|
-
}).promise();
|
|
312
|
-
|
|
313
|
-
return JSON.parse(state.Body.toString());
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async saveState(stackName: string, state: any): Promise<void> {
|
|
317
|
-
await this.s3.putObject({
|
|
318
|
-
Bucket: 'my-state-bucket',
|
|
319
|
-
Key: `${stackName}.json`,
|
|
320
|
-
Body: JSON.stringify(state)
|
|
321
|
-
}).promise();
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## Real-World Example
|
|
327
|
-
|
|
328
|
-
Here's how the 5-layer enterprise application from the examples works:
|
|
329
|
-
|
|
330
|
-
```tsx
|
|
331
|
-
function EnterpriseApp() {
|
|
332
|
-
return (
|
|
333
|
-
<NetworkProvider> {/* VPC, subnets, security groups */}
|
|
334
|
-
<DatabaseProvider> {/* Database + cache */}
|
|
335
|
-
<StorageProvider> {/* S3 + CDN */}
|
|
336
|
-
<ApiProvider> {/* APIs + load balancer */}
|
|
337
|
-
<MonitoringProvider /> {/* Analytics + backups */}
|
|
338
|
-
</ApiProvider>
|
|
339
|
-
</StorageProvider>
|
|
340
|
-
</DatabaseProvider>
|
|
341
|
-
</NetworkProvider>
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Each provider component:
|
|
346
|
-
function NetworkProvider({ children }) {
|
|
347
|
-
const vpc = useInstance(VPC, { cidr: '10.0.0.0/16' });
|
|
348
|
-
|
|
349
|
-
return (
|
|
350
|
-
<NetworkContext.Provider value={{ vpcId: vpc.outputs?.vpcId }}>
|
|
351
|
-
{children}
|
|
352
|
-
</NetworkContext.Provider>
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function DatabaseProvider({ children }) {
|
|
357
|
-
const { vpcId } = useContext(NetworkContext);
|
|
358
|
-
|
|
359
|
-
const db = useInstance(Database, { vpcId });
|
|
360
|
-
const cache = useInstance(Redis, { vpcId });
|
|
361
|
-
|
|
362
|
-
return (
|
|
363
|
-
<DatabaseContext.Provider value={{
|
|
364
|
-
dbUrl: db.outputs?.endpoint,
|
|
365
|
-
cacheUrl: cache.outputs?.endpoint
|
|
366
|
-
}}>
|
|
367
|
-
{children}
|
|
368
|
-
</DatabaseContext.Provider>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
25
|
|
|
373
26
|
## Installation & Usage
|
|
374
27
|
|
|
@@ -129,7 +129,6 @@ class DevCommand extends BaseCommand_1.BaseCommand {
|
|
|
129
129
|
try {
|
|
130
130
|
const backendState = await result.instance.getBackendProvider().getState(result.stackName);
|
|
131
131
|
previousCloudDOM = backendState?.cloudDOM || [];
|
|
132
|
-
logger.debug(`DevCommand: Previous CloudDOM loaded with ${previousCloudDOM.length} resources`);
|
|
133
132
|
}
|
|
134
133
|
catch (error) {
|
|
135
134
|
logger.debug(`DevCommand: Could not load previous state: ${error.message}`);
|
|
@@ -184,9 +183,19 @@ class DevCommand extends BaseCommand_1.BaseCommand {
|
|
|
184
183
|
// Create new instance
|
|
185
184
|
const result = await CLIContext_1.CLIContextManager.createCLIInstance(entryPath, this.verbose);
|
|
186
185
|
logger.debug(`DevCommand: Hot reload CloudDOM built with ${result.cloudDOM.length} resources`);
|
|
186
|
+
// Load drift-corrected state for comparison
|
|
187
|
+
let previousCloudDOM = this.state.lastCloudDOM;
|
|
188
|
+
try {
|
|
189
|
+
const backendState = await result.instance.getBackendProvider().getState(result.stackName);
|
|
190
|
+
previousCloudDOM = backendState?.cloudDOM || this.state.lastCloudDOM;
|
|
191
|
+
logger.debug(`DevCommand: Comparing against drift-corrected state with ${previousCloudDOM?.length || 0} resources`);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
logger.debug(`DevCommand: Using cached state for comparison: ${error.message}`);
|
|
195
|
+
}
|
|
187
196
|
// Compute diff using Reconciler
|
|
188
197
|
const reconciler = new Reconciler_1.Reconciler();
|
|
189
|
-
const changeSet = reconciler.reconcile(
|
|
198
|
+
const changeSet = reconciler.reconcile(previousCloudDOM, result.cloudDOM);
|
|
190
199
|
const totalChanges = (0, Reconciler_1.getTotalChanges)(changeSet);
|
|
191
200
|
logger.debug(`DevCommand: Hot reload total changes: ${totalChanges}`);
|
|
192
201
|
// Always update state to preserve reactive changes
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/core/CReact.js
CHANGED
|
@@ -1096,9 +1096,28 @@ class CReact {
|
|
|
1096
1096
|
CReact._lastInstance = instance;
|
|
1097
1097
|
CReact._lastElement = element;
|
|
1098
1098
|
CReact._lastStackName = stackName;
|
|
1099
|
+
// Initialize providers if they have an initialize method
|
|
1100
|
+
if (CReact.cloudProvider.initialize) {
|
|
1101
|
+
await CReact.cloudProvider.initialize();
|
|
1102
|
+
}
|
|
1103
|
+
if (CReact.backendProvider.initialize) {
|
|
1104
|
+
await CReact.backendProvider.initialize();
|
|
1105
|
+
}
|
|
1099
1106
|
// CRITICAL: Load previous state from backend before rendering
|
|
1100
1107
|
// This enables useState to hydrate from persisted state
|
|
1101
1108
|
await instance.loadStateForHydration(stackName);
|
|
1109
|
+
// CRITICAL: Detect and fix drift before rendering
|
|
1110
|
+
// This ensures state matches reality and prevents stale deployments
|
|
1111
|
+
const driftResult = await instance.stateMachine.detectAndFixDrift(stackName, CReact.cloudProvider);
|
|
1112
|
+
// If drift was detected and fixed, reload state to get updated outputs
|
|
1113
|
+
if (driftResult.resourcesFixed > 0) {
|
|
1114
|
+
logger.debug(`Reloading state after fixing ${driftResult.resourcesFixed} drifted resources`);
|
|
1115
|
+
// Clear existing hydration data and reload with fresh state
|
|
1116
|
+
const freshState = await instance.stateMachine.getState(stackName);
|
|
1117
|
+
if (freshState?.cloudDOM) {
|
|
1118
|
+
instance.prepareHydration(freshState.cloudDOM, true); // clearExisting = true
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1102
1121
|
// Build and return CloudDOM
|
|
1103
1122
|
return instance.build(element, stackName);
|
|
1104
1123
|
}
|
package/dist/core/Reconciler.js
CHANGED
|
@@ -580,6 +580,13 @@ class Reconciler {
|
|
|
580
580
|
});
|
|
581
581
|
return 'replacement';
|
|
582
582
|
}
|
|
583
|
+
// DRIFT DETECTION: If previous node has no outputs, it needs redeployment
|
|
584
|
+
// This happens when drift detection clears outputs for dead resources
|
|
585
|
+
// Treat as update to trigger redeployment
|
|
586
|
+
if (!previous.outputs || Object.keys(previous.outputs).length === 0) {
|
|
587
|
+
logger.debug(`Node ${current.id} has no outputs (drift detected) - needs redeployment`);
|
|
588
|
+
return 'update';
|
|
589
|
+
}
|
|
583
590
|
// Hash-based diff acceleration: compute or reuse cached hashes
|
|
584
591
|
const prevHash = ((_a = previous)._propHash ?? (_a._propHash = this.computeShallowHash(previous.props)));
|
|
585
592
|
const currHash = ((_b = current)._propHash ?? (_b._propHash = this.computeShallowHash(current.props)));
|
|
@@ -421,4 +421,21 @@ export declare class StateMachine {
|
|
|
421
421
|
changeSet?: ChangeSet;
|
|
422
422
|
cloudDOM?: CloudDOMNode[];
|
|
423
423
|
}>;
|
|
424
|
+
/**
|
|
425
|
+
* Detect and fix drift in deployed resources
|
|
426
|
+
*
|
|
427
|
+
* Checks if resources in the backend state still match reality.
|
|
428
|
+
* If drift is detected, refreshes the state to reflect actual cloud state.
|
|
429
|
+
*
|
|
430
|
+
* This is called automatically during state load to ensure state accuracy.
|
|
431
|
+
*
|
|
432
|
+
* @param stackName - Stack name to check for drift
|
|
433
|
+
* @param cloudProvider - Cloud provider with drift detection capabilities
|
|
434
|
+
* @returns Promise resolving to drift detection results
|
|
435
|
+
*/
|
|
436
|
+
detectAndFixDrift(stackName: string, cloudProvider: import('../providers/ICloudProvider').ICloudProvider): Promise<{
|
|
437
|
+
driftDetected: boolean;
|
|
438
|
+
driftResults: import('../providers/ICloudProvider').DriftDetectionResult[];
|
|
439
|
+
resourcesFixed: number;
|
|
440
|
+
}>;
|
|
424
441
|
}
|
|
@@ -771,6 +771,102 @@ class StateMachine {
|
|
|
771
771
|
return { action: 'rolled_back' };
|
|
772
772
|
}
|
|
773
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Detect and fix drift in deployed resources
|
|
776
|
+
*
|
|
777
|
+
* Checks if resources in the backend state still match reality.
|
|
778
|
+
* If drift is detected, refreshes the state to reflect actual cloud state.
|
|
779
|
+
*
|
|
780
|
+
* This is called automatically during state load to ensure state accuracy.
|
|
781
|
+
*
|
|
782
|
+
* @param stackName - Stack name to check for drift
|
|
783
|
+
* @param cloudProvider - Cloud provider with drift detection capabilities
|
|
784
|
+
* @returns Promise resolving to drift detection results
|
|
785
|
+
*/
|
|
786
|
+
async detectAndFixDrift(stackName, cloudProvider) {
|
|
787
|
+
const state = await this.getState(stackName);
|
|
788
|
+
if (!state?.cloudDOM) {
|
|
789
|
+
return { driftDetected: false, driftResults: [], resourcesFixed: 0 };
|
|
790
|
+
}
|
|
791
|
+
logger.debug(`Detecting drift for stack: ${stackName}`);
|
|
792
|
+
const driftResults = [];
|
|
793
|
+
let resourcesFixed = 0;
|
|
794
|
+
for (const node of state.cloudDOM) {
|
|
795
|
+
// Skip nodes without outputs (not yet deployed)
|
|
796
|
+
if (!node.outputs) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
// Detect drift (required method)
|
|
800
|
+
const result = await cloudProvider.detectDrift(node);
|
|
801
|
+
driftResults.push(result);
|
|
802
|
+
if (result.hasDrifted) {
|
|
803
|
+
logger.info(`Drift detected: ${node.id} - ${result.driftDescription || 'State mismatch'}`);
|
|
804
|
+
// Refresh state to fix drift (required method)
|
|
805
|
+
logger.debug(`Refreshing state for: ${node.id}`);
|
|
806
|
+
await cloudProvider.refreshState(node);
|
|
807
|
+
resourcesFixed++;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// If any drift was detected, clear outputs for drifted resources and their children
|
|
811
|
+
// With the "one useInstance per component" constraint, dependencies = nesting
|
|
812
|
+
// So clearing a drifted node + its children ensures complete redeployment
|
|
813
|
+
if (resourcesFixed > 0) {
|
|
814
|
+
const driftedNodeIds = new Set(driftResults.filter(r => r.hasDrifted).map(r => r.nodeId));
|
|
815
|
+
logger.info(`Clearing outputs for ${driftedNodeIds.size} drifted resources and their children`);
|
|
816
|
+
// Clear outputs for drifted nodes and all their descendants
|
|
817
|
+
const clearDriftedOutputs = (nodes) => {
|
|
818
|
+
for (const node of nodes) {
|
|
819
|
+
const isDrifted = driftedNodeIds.has(node.id);
|
|
820
|
+
if (isDrifted && node.outputs) {
|
|
821
|
+
logger.debug(`Clearing outputs for drifted resource: ${node.id}`);
|
|
822
|
+
node.outputs = undefined;
|
|
823
|
+
}
|
|
824
|
+
// If this node is drifted, clear all its children too
|
|
825
|
+
if (node.children) {
|
|
826
|
+
if (isDrifted) {
|
|
827
|
+
// Clear all children of drifted nodes
|
|
828
|
+
const clearAllChildren = (childNodes) => {
|
|
829
|
+
for (const child of childNodes) {
|
|
830
|
+
if (child.outputs) {
|
|
831
|
+
logger.debug(`Clearing outputs for child of drifted resource: ${child.id}`);
|
|
832
|
+
child.outputs = undefined;
|
|
833
|
+
}
|
|
834
|
+
if (child.children) {
|
|
835
|
+
clearAllChildren(child.children);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
clearAllChildren(node.children);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
// Continue searching for drifted nodes in children
|
|
843
|
+
clearDriftedOutputs(node.children);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
clearDriftedOutputs(state.cloudDOM);
|
|
849
|
+
}
|
|
850
|
+
// If drift was detected and state was refreshed, save updated state
|
|
851
|
+
const driftDetected = driftResults.some(r => r.hasDrifted);
|
|
852
|
+
if (driftDetected && resourcesFixed > 0) {
|
|
853
|
+
logger.info(`Saving refreshed state after fixing ${resourcesFixed} drifted resources`);
|
|
854
|
+
await this.withRetry(() => this.backendProvider.saveState(stackName, {
|
|
855
|
+
...state,
|
|
856
|
+
cloudDOM: state.cloudDOM,
|
|
857
|
+
timestamp: Date.now(),
|
|
858
|
+
}));
|
|
859
|
+
// Log drift detection to audit trail
|
|
860
|
+
await this.logAction(stackName, 'checkpoint', state);
|
|
861
|
+
}
|
|
862
|
+
if (driftDetected) {
|
|
863
|
+
logger.info(`Drift detection complete: ${driftResults.filter(r => r.hasDrifted).length} resources drifted, ${resourcesFixed} fixed`);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
logger.debug('No drift detected');
|
|
867
|
+
}
|
|
868
|
+
return { driftDetected, driftResults, resourcesFixed };
|
|
869
|
+
}
|
|
774
870
|
}
|
|
775
871
|
exports.StateMachine = StateMachine;
|
|
776
872
|
/**
|
|
@@ -248,6 +248,40 @@ function useInstance(construct, props) {
|
|
|
248
248
|
const { currentPath, previousOutputsMap } = context;
|
|
249
249
|
// Get hook index for this useInstance call (instance-specific)
|
|
250
250
|
const hookIndex = (0, context_1.incrementHookIndex)('instance');
|
|
251
|
+
// CONSTRAINT: Only one useInstance per component
|
|
252
|
+
// This simplifies dependency tracking and drift recovery
|
|
253
|
+
if (hookIndex > 0) {
|
|
254
|
+
const componentName = currentFiber.type?.name || 'Anonymous';
|
|
255
|
+
throw new Error(`[CReact Constraint] Only one useInstance call is allowed per component.\n\n` +
|
|
256
|
+
`Component: ${componentName}\n` +
|
|
257
|
+
`Path: ${currentPath.join('.')}\n\n` +
|
|
258
|
+
`This constraint ensures:\n` +
|
|
259
|
+
` 1. Clear resource dependencies (parent-child nesting)\n` +
|
|
260
|
+
` 2. Simpler drift recovery (clear drifted node + children)\n` +
|
|
261
|
+
` 3. Better component composition\n\n` +
|
|
262
|
+
`Solution: Split your component into multiple components, one per resource.\n\n` +
|
|
263
|
+
`Example:\n` +
|
|
264
|
+
` ❌ function Stack() {\n` +
|
|
265
|
+
` const db = useInstance(Database, {...});\n` +
|
|
266
|
+
` const api = useInstance(API, {...}); // Error!\n` +
|
|
267
|
+
` }\n\n` +
|
|
268
|
+
` ✅ function Stack() {\n` +
|
|
269
|
+
` return (\n` +
|
|
270
|
+
` <>\n` +
|
|
271
|
+
` <PrimaryDatabase />\n` +
|
|
272
|
+
` <ApiServer />\n` +
|
|
273
|
+
` </>\n` +
|
|
274
|
+
` );\n` +
|
|
275
|
+
` }\n` +
|
|
276
|
+
` function PrimaryDatabase() {\n` +
|
|
277
|
+
` const db = useInstance(Database, {...});\n` +
|
|
278
|
+
` return <></>;\n` +
|
|
279
|
+
` }\n` +
|
|
280
|
+
` function ApiServer() {\n` +
|
|
281
|
+
` const api = useInstance(API, {...});\n` +
|
|
282
|
+
` return <></>;\n` +
|
|
283
|
+
` }\n`);
|
|
284
|
+
}
|
|
251
285
|
// Extract key from props (React-like)
|
|
252
286
|
const { key, ...restProps } = props;
|
|
253
287
|
// Check for undefined dependencies - enforce deployment order
|
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export { Renderer } from './core/Renderer';
|
|
|
36
36
|
export { Validator } from './core/Validator';
|
|
37
37
|
export { CloudDOMBuilder } from './core/CloudDOMBuilder';
|
|
38
38
|
export { FiberNode, CloudDOMNode } from './core/types';
|
|
39
|
-
export { ICloudProvider } from './providers/ICloudProvider';
|
|
39
|
+
export { ICloudProvider, DriftDetectionResult, OutputChangeEvent } from './providers/ICloudProvider';
|
|
40
40
|
export { IBackendProvider } from './providers/IBackendProvider';
|
|
41
41
|
export { useInstance } from './hooks/useInstance';
|
|
42
42
|
export { useState } from './hooks/useState';
|
|
@@ -40,6 +40,23 @@ export interface OutputChangeEvent {
|
|
|
40
40
|
/** Timestamp of the change */
|
|
41
41
|
timestamp: number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* DriftDetectionResult represents the result of checking if a resource has drifted
|
|
45
|
+
*/
|
|
46
|
+
export interface DriftDetectionResult {
|
|
47
|
+
/** Resource ID that was checked */
|
|
48
|
+
nodeId: string;
|
|
49
|
+
/** Whether the resource has drifted from expected state */
|
|
50
|
+
hasDrifted: boolean;
|
|
51
|
+
/** Expected state from CloudDOM */
|
|
52
|
+
expectedState?: Record<string, any>;
|
|
53
|
+
/** Actual state from cloud provider */
|
|
54
|
+
actualState?: Record<string, any>;
|
|
55
|
+
/** Human-readable description of the drift */
|
|
56
|
+
driftDescription?: string;
|
|
57
|
+
/** Timestamp of the check */
|
|
58
|
+
timestamp: number;
|
|
59
|
+
}
|
|
43
60
|
/**
|
|
44
61
|
* ICloudProvider defines the interface for cloud infrastructure providers.
|
|
45
62
|
* Implementations materialize CloudDOM trees into actual cloud resources.
|
|
@@ -143,4 +160,71 @@ export interface ICloudProvider {
|
|
|
143
160
|
* @param change - Output change details
|
|
144
161
|
*/
|
|
145
162
|
emit?(event: 'outputsChanged', change: OutputChangeEvent): void;
|
|
163
|
+
/**
|
|
164
|
+
* Detect drift for a specific resource (REQUIRED)
|
|
165
|
+
*
|
|
166
|
+
* Compares the expected state (from CloudDOM) with the actual state
|
|
167
|
+
* (from the cloud provider) to detect if the resource has drifted.
|
|
168
|
+
*
|
|
169
|
+
* This is called by CReact automatically during:
|
|
170
|
+
* - Every state load (to detect stale state)
|
|
171
|
+
* - Plan command (to show drift before deployment)
|
|
172
|
+
* - Deploy command (to ensure state accuracy)
|
|
173
|
+
*
|
|
174
|
+
* Providers MUST implement this to ensure state accuracy.
|
|
175
|
+
*
|
|
176
|
+
* @param node - CloudDOM node representing expected state
|
|
177
|
+
* @returns Promise resolving to drift detection result
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* async detectDrift(node: CloudDOMNode): Promise<DriftDetectionResult> {
|
|
182
|
+
* // For resources without outputs, no drift possible
|
|
183
|
+
* if (!node.outputs) {
|
|
184
|
+
* return { nodeId: node.id, hasDrifted: false, timestamp: Date.now() };
|
|
185
|
+
* }
|
|
186
|
+
*
|
|
187
|
+
* const actualState = await this.getActualResourceState(node.id);
|
|
188
|
+
* const hasDrifted = !this.statesMatch(node.outputs, actualState);
|
|
189
|
+
*
|
|
190
|
+
* return {
|
|
191
|
+
* nodeId: node.id,
|
|
192
|
+
* hasDrifted,
|
|
193
|
+
* expectedState: node.outputs,
|
|
194
|
+
* actualState,
|
|
195
|
+
* driftDescription: hasDrifted ? 'Resource no longer exists' : undefined,
|
|
196
|
+
* timestamp: Date.now(),
|
|
197
|
+
* };
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
detectDrift(node: CloudDOMNode): Promise<DriftDetectionResult>;
|
|
202
|
+
/**
|
|
203
|
+
* Refresh resource state from actual cloud provider (REQUIRED)
|
|
204
|
+
*
|
|
205
|
+
* Queries the actual state of a resource and updates the node's outputs
|
|
206
|
+
* to reflect reality. This is the mechanism for fixing drift.
|
|
207
|
+
*
|
|
208
|
+
* Called automatically by CReact when drift is detected.
|
|
209
|
+
*
|
|
210
|
+
* Providers MUST implement this to enable automatic drift recovery.
|
|
211
|
+
*
|
|
212
|
+
* @param node - CloudDOM node to refresh
|
|
213
|
+
* @returns Promise resolving when refresh is complete (node.outputs updated)
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* async refreshState(node: CloudDOMNode): Promise<void> {
|
|
218
|
+
* const actualState = await this.getActualResourceState(node.id);
|
|
219
|
+
* if (actualState) {
|
|
220
|
+
* // Resource exists - update outputs to match reality
|
|
221
|
+
* node.outputs = actualState;
|
|
222
|
+
* } else {
|
|
223
|
+
* // Resource doesn't exist - clear outputs to force redeployment
|
|
224
|
+
* node.outputs = undefined;
|
|
225
|
+
* }
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
refreshState(node: CloudDOMNode): Promise<void>;
|
|
146
230
|
}
|