@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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creact-labs/creact",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CReact - React for Infrastructure. Render JSX to CloudDOM.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",