@creact-labs/creact 0.1.5 → 0.1.6
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/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
|
}
|
|
@@ -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
|
}
|