@creact-labs/creact 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +73 -22
  2. package/dist/cli.d.ts +11 -0
  3. package/dist/cli.js +88 -0
  4. package/dist/index.d.ts +19 -44
  5. package/dist/index.js +20 -68
  6. package/dist/jsx/index.d.ts +2 -0
  7. package/dist/jsx/index.js +1 -0
  8. package/dist/jsx/jsx-dev-runtime.d.ts +4 -0
  9. package/dist/jsx/jsx-dev-runtime.js +4 -0
  10. package/dist/jsx/jsx-runtime.d.ts +38 -0
  11. package/dist/jsx/jsx-runtime.js +38 -0
  12. package/dist/jsx/types.d.ts +12 -0
  13. package/dist/jsx/types.js +4 -0
  14. package/dist/primitives/context.d.ts +34 -0
  15. package/dist/primitives/context.js +63 -0
  16. package/dist/primitives/index.d.ts +3 -0
  17. package/dist/primitives/index.js +3 -0
  18. package/dist/primitives/instance.d.ts +72 -0
  19. package/dist/primitives/instance.js +235 -0
  20. package/dist/primitives/store.d.ts +22 -0
  21. package/dist/primitives/store.js +97 -0
  22. package/dist/provider/backend.d.ts +110 -0
  23. package/dist/provider/backend.js +37 -0
  24. package/dist/provider/interface.d.ts +48 -0
  25. package/dist/provider/interface.js +39 -0
  26. package/dist/reactive/effect.d.ts +11 -0
  27. package/dist/reactive/effect.js +42 -0
  28. package/dist/reactive/index.d.ts +3 -0
  29. package/dist/reactive/index.js +3 -0
  30. package/dist/reactive/signal.d.ts +32 -0
  31. package/dist/reactive/signal.js +60 -0
  32. package/dist/reactive/tracking.d.ts +41 -0
  33. package/dist/reactive/tracking.js +161 -0
  34. package/dist/runtime/fiber.d.ts +21 -0
  35. package/dist/runtime/fiber.js +16 -0
  36. package/dist/runtime/index.d.ts +4 -0
  37. package/dist/runtime/index.js +4 -0
  38. package/dist/runtime/reconcile.d.ts +66 -0
  39. package/dist/runtime/reconcile.js +210 -0
  40. package/dist/runtime/render.d.ts +42 -0
  41. package/dist/runtime/render.js +231 -0
  42. package/dist/runtime/run.d.ts +119 -0
  43. package/dist/runtime/run.js +334 -0
  44. package/dist/runtime/state-machine.d.ts +95 -0
  45. package/dist/runtime/state-machine.js +209 -0
  46. package/dist/types.d.ts +13 -0
  47. package/dist/types.js +4 -0
  48. package/package.json +11 -24
  49. package/dist/cli/commands/BuildCommand.d.ts +0 -40
  50. package/dist/cli/commands/BuildCommand.js +0 -151
  51. package/dist/cli/commands/DeployCommand.d.ts +0 -38
  52. package/dist/cli/commands/DeployCommand.js +0 -194
  53. package/dist/cli/commands/DevCommand.d.ts +0 -52
  54. package/dist/cli/commands/DevCommand.js +0 -394
  55. package/dist/cli/commands/PlanCommand.d.ts +0 -39
  56. package/dist/cli/commands/PlanCommand.js +0 -164
  57. package/dist/cli/commands/index.d.ts +0 -36
  58. package/dist/cli/commands/index.js +0 -43
  59. package/dist/cli/core/ArgumentParser.d.ts +0 -46
  60. package/dist/cli/core/ArgumentParser.js +0 -127
  61. package/dist/cli/core/BaseCommand.d.ts +0 -75
  62. package/dist/cli/core/BaseCommand.js +0 -95
  63. package/dist/cli/core/CLIContext.d.ts +0 -68
  64. package/dist/cli/core/CLIContext.js +0 -183
  65. package/dist/cli/core/CommandRegistry.d.ts +0 -64
  66. package/dist/cli/core/CommandRegistry.js +0 -89
  67. package/dist/cli/core/index.d.ts +0 -36
  68. package/dist/cli/core/index.js +0 -43
  69. package/dist/cli/index.d.ts +0 -35
  70. package/dist/cli/index.js +0 -100
  71. package/dist/cli/output.d.ts +0 -204
  72. package/dist/cli/output.js +0 -437
  73. package/dist/cli/utils.d.ts +0 -59
  74. package/dist/cli/utils.js +0 -76
  75. package/dist/context/createContext.d.ts +0 -90
  76. package/dist/context/createContext.js +0 -113
  77. package/dist/context/index.d.ts +0 -30
  78. package/dist/context/index.js +0 -35
  79. package/dist/core/CReact.d.ts +0 -409
  80. package/dist/core/CReact.js +0 -1151
  81. package/dist/core/CloudDOMBuilder.d.ts +0 -447
  82. package/dist/core/CloudDOMBuilder.js +0 -1234
  83. package/dist/core/ContextDependencyTracker.d.ts +0 -165
  84. package/dist/core/ContextDependencyTracker.js +0 -448
  85. package/dist/core/ErrorRecoveryManager.d.ts +0 -145
  86. package/dist/core/ErrorRecoveryManager.js +0 -443
  87. package/dist/core/EventBus.d.ts +0 -91
  88. package/dist/core/EventBus.js +0 -185
  89. package/dist/core/ProviderOutputTracker.d.ts +0 -211
  90. package/dist/core/ProviderOutputTracker.js +0 -476
  91. package/dist/core/ReactiveUpdateQueue.d.ts +0 -76
  92. package/dist/core/ReactiveUpdateQueue.js +0 -121
  93. package/dist/core/Reconciler.d.ts +0 -415
  94. package/dist/core/Reconciler.js +0 -1044
  95. package/dist/core/RenderScheduler.d.ts +0 -153
  96. package/dist/core/RenderScheduler.js +0 -519
  97. package/dist/core/Renderer.d.ts +0 -336
  98. package/dist/core/Renderer.js +0 -944
  99. package/dist/core/Runtime.d.ts +0 -246
  100. package/dist/core/Runtime.js +0 -640
  101. package/dist/core/StateBindingManager.d.ts +0 -121
  102. package/dist/core/StateBindingManager.js +0 -309
  103. package/dist/core/StateMachine.d.ts +0 -441
  104. package/dist/core/StateMachine.js +0 -883
  105. package/dist/core/StructuralChangeDetector.d.ts +0 -140
  106. package/dist/core/StructuralChangeDetector.js +0 -363
  107. package/dist/core/Validator.d.ts +0 -127
  108. package/dist/core/Validator.js +0 -279
  109. package/dist/core/errors.d.ts +0 -153
  110. package/dist/core/errors.js +0 -202
  111. package/dist/core/index.d.ts +0 -38
  112. package/dist/core/index.js +0 -64
  113. package/dist/core/types.d.ts +0 -265
  114. package/dist/core/types.js +0 -48
  115. package/dist/hooks/context.d.ts +0 -147
  116. package/dist/hooks/context.js +0 -334
  117. package/dist/hooks/useContext.d.ts +0 -113
  118. package/dist/hooks/useContext.js +0 -169
  119. package/dist/hooks/useEffect.d.ts +0 -105
  120. package/dist/hooks/useEffect.js +0 -540
  121. package/dist/hooks/useInstance.d.ts +0 -139
  122. package/dist/hooks/useInstance.js +0 -455
  123. package/dist/hooks/useState.d.ts +0 -120
  124. package/dist/hooks/useState.js +0 -298
  125. package/dist/jsx.d.ts +0 -143
  126. package/dist/jsx.js +0 -76
  127. package/dist/providers/DummyBackendProvider.d.ts +0 -193
  128. package/dist/providers/DummyBackendProvider.js +0 -189
  129. package/dist/providers/DummyCloudProvider.d.ts +0 -128
  130. package/dist/providers/DummyCloudProvider.js +0 -157
  131. package/dist/providers/IBackendProvider.d.ts +0 -177
  132. package/dist/providers/IBackendProvider.js +0 -31
  133. package/dist/providers/ICloudProvider.d.ts +0 -230
  134. package/dist/providers/ICloudProvider.js +0 -31
  135. package/dist/providers/index.d.ts +0 -31
  136. package/dist/providers/index.js +0 -31
  137. package/dist/test-event-callbacks.d.ts +0 -0
  138. package/dist/test-event-callbacks.js +0 -1
  139. package/dist/utils/Logger.d.ts +0 -144
  140. package/dist/utils/Logger.js +0 -220
  141. package/dist/utils/Output.d.ts +0 -161
  142. package/dist/utils/Output.js +0 -401
  143. package/dist/utils/deepEqual.d.ts +0 -71
  144. package/dist/utils/deepEqual.js +0 -276
  145. package/dist/utils/naming.d.ts +0 -241
  146. package/dist/utils/naming.js +0 -376
@@ -1,883 +0,0 @@
1
- "use strict";
2
- /**
3
-
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
-
6
- * you may not use this file except in compliance with the License.
7
-
8
- * You may obtain a copy of the License at
9
-
10
- *
11
-
12
- * http://www.apache.org/licenses/LICENSE-2.0
13
-
14
- *
15
-
16
- * Unless required by applicable law or agreed to in writing, software
17
-
18
- * distributed under the License is distributed on an "AS IS" BASIS,
19
-
20
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
-
22
- * See the License for the specific language governing permissions and
23
-
24
- * limitations under the License.
25
-
26
- *
27
-
28
- * Copyright 2025 Daniel Coutinho Ribeiro
29
-
30
- */
31
- Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.StateMachine = void 0;
33
- const errors_1 = require("./errors");
34
- const Logger_1 = require("../utils/Logger");
35
- const logger = Logger_1.LoggerFactory.getLogger('state-machine');
36
- /**
37
- * StateMachine manages deployment lifecycle with crash recovery
38
- *
39
- * Responsibilities:
40
- * - Track deployment state transitions
41
- * - Save checkpoints after each resource deploys
42
- * - Enable crash recovery (resume or rollback)
43
- * - Provide atomic state persistence via BackendProvider
44
- * - Prevent concurrent deployments via locking (REQ-O02)
45
- * - Emit audit trail for compliance (REQ-O05)
46
- * - Retry transient backend failures (REQ-O03)
47
- *
48
- * Design principles:
49
- * - All state changes are atomic (saved to BackendProvider immediately)
50
- * - Checkpoints enable resume from any point
51
- * - Rollback applies reverse change set
52
- * - State machine is universal (works with all providers)
53
- * - Locking prevents race conditions
54
- * - Audit trail provides compliance and debugging
55
- *
56
- * REQ-O01: CloudDOM State Machine
57
- * REQ-O02: State Locking
58
- * REQ-O03: Retry Logic
59
- * REQ-O05: Audit Log
60
- * REQ-ARCH-01: Provider-Orchestration Separation
61
- *
62
- * @example
63
- * ```typescript
64
- * const stateMachine = new StateMachine(backendProvider);
65
- *
66
- * // Listen to events
67
- * stateMachine.on('checkpoint', (state) => {
68
- * logger.info(`Checkpoint: ${state.checkpoint}`);
69
- * });
70
- *
71
- * // Start deployment (acquires lock)
72
- * await stateMachine.startDeployment('my-stack', changeSet, cloudDOM, 'user@example.com');
73
- *
74
- * // Update checkpoint after each resource
75
- * await stateMachine.updateCheckpoint('my-stack', 0);
76
- * await stateMachine.updateCheckpoint('my-stack', 1);
77
- *
78
- * // Complete deployment (releases lock)
79
- * await stateMachine.completeDeployment('my-stack');
80
- *
81
- * // Or handle failure (releases lock)
82
- * await stateMachine.failDeployment('my-stack', new DeploymentError('Provider timeout'));
83
- * ```
84
- */
85
- class StateMachine {
86
- constructor(backendProvider, options = {}) {
87
- this.backendProvider = backendProvider;
88
- this.options = options;
89
- this.listeners = {
90
- started: [],
91
- checkpoint: [],
92
- completed: [],
93
- failed: [],
94
- rolled_back: [],
95
- };
96
- /**
97
- * Map of active lock renewal timers by stack name
98
- * Used to clean up timers when deployment completes
99
- */
100
- this.lockRenewalTimers = new Map();
101
- this.options = {
102
- maxRetries: 3,
103
- enableAuditLog: true,
104
- enableSnapshots: false,
105
- lockTTL: 600,
106
- ...options,
107
- };
108
- }
109
- /**
110
- * Subscribe to state machine events
111
- *
112
- * Enables observability and telemetry integration.
113
- *
114
- * @param event - Event type to listen for
115
- * @param handler - Callback function receiving structured payload
116
- */
117
- on(event, handler) {
118
- this.listeners[event].push(handler);
119
- }
120
- /**
121
- * Unsubscribe from state machine events
122
- *
123
- * @param event - Event type to stop listening for
124
- * @param handler - Callback function to remove
125
- */
126
- off(event, handler) {
127
- const index = this.listeners[event].indexOf(handler);
128
- if (index !== -1) {
129
- this.listeners[event].splice(index, 1);
130
- }
131
- }
132
- /**
133
- * Emit event to all listeners with structured payload
134
- *
135
- * @param event - Event type
136
- * @param state - Current deployment state
137
- * @param metadata - Additional metadata (optional)
138
- */
139
- emit(event, state, metadata) {
140
- const payload = {
141
- event,
142
- state,
143
- metadata,
144
- };
145
- for (const handler of this.listeners[event]) {
146
- try {
147
- handler(payload);
148
- }
149
- catch (error) {
150
- // Don't let listener errors break state machine
151
- logger.error(`Error in StateMachine event handler for ${event}:`, error);
152
- }
153
- }
154
- }
155
- /**
156
- * Start lock auto-renewal for a stack
157
- *
158
- * Renews the lock at 50% of the TTL interval to prevent expiration
159
- * during long deployments. Only starts renewal if backend supports locking.
160
- *
161
- * @param stackName - Name of the stack
162
- * @param holder - Lock holder identifier
163
- * @param ttl - Lock TTL in seconds
164
- */
165
- startLockRenewal(stackName, holder, ttl) {
166
- // Only start renewal if backend supports locking
167
- if (!this.backendProvider.acquireLock) {
168
- return;
169
- }
170
- // Clear any existing timer for this stack
171
- this.stopLockRenewal(stackName);
172
- // Renew at 50% of TTL (convert to milliseconds)
173
- const renewalInterval = (ttl * 1000) / 2;
174
- const timer = setInterval(async () => {
175
- try {
176
- // Renew the lock with the same TTL
177
- await this.backendProvider.acquireLock(stackName, holder, ttl);
178
- logger.debug(`Lock renewed for stack: ${stackName}`);
179
- }
180
- catch (error) {
181
- logger.error(`Failed to renew lock for ${stackName}:`, error);
182
- // Stop renewal on failure to prevent spam
183
- this.stopLockRenewal(stackName);
184
- }
185
- }, renewalInterval);
186
- this.lockRenewalTimers.set(stackName, timer);
187
- logger.debug(`Lock auto-renewal started for stack: ${stackName} (interval: ${renewalInterval}ms)`);
188
- }
189
- /**
190
- * Stop lock auto-renewal for a stack
191
- *
192
- * Cleans up the renewal timer to prevent memory leaks.
193
- *
194
- * @param stackName - Name of the stack
195
- */
196
- stopLockRenewal(stackName) {
197
- const timer = this.lockRenewalTimers.get(stackName);
198
- if (timer) {
199
- clearInterval(timer);
200
- this.lockRenewalTimers.delete(stackName);
201
- logger.debug(`Lock auto-renewal stopped for stack: ${stackName}`);
202
- }
203
- }
204
- /**
205
- * Validate state transition
206
- *
207
- * Ensures state machine only transitions through valid states.
208
- * Prevents accidental invalid state mutations.
209
- *
210
- * @param from - Current state
211
- * @param to - Target state
212
- * @throws DeploymentError if transition is invalid
213
- */
214
- validateTransition(from, to) {
215
- const allowedTransitions = StateMachine.VALID_TRANSITIONS[from];
216
- if (!allowedTransitions || !allowedTransitions.includes(to)) {
217
- throw new errors_1.DeploymentError(`Invalid state transition: ${from} → ${to}`, {
218
- message: `Invalid state transition: ${from} → ${to}`,
219
- code: 'INVALID_STATE_TRANSITION',
220
- details: {
221
- from,
222
- to,
223
- allowedTransitions,
224
- },
225
- });
226
- }
227
- }
228
- /**
229
- * Retry operation with exponential backoff
230
- *
231
- * Handles transient backend failures (network issues, timeouts, etc.)
232
- *
233
- * REQ-O03: Error handling and retry logic
234
- *
235
- * @param operation - Async operation to retry
236
- * @param retries - Number of retries (default: from options)
237
- * @returns Promise resolving to operation result
238
- * @throws Error if all retries fail
239
- */
240
- async withRetry(operation, retries = this.options.maxRetries ?? 3) {
241
- let lastError;
242
- for (let i = 0; i < retries; i++) {
243
- try {
244
- return await operation();
245
- }
246
- catch (err) {
247
- lastError = err;
248
- // Don't retry on final attempt
249
- if (i < retries - 1) {
250
- // Exponential backoff: 100ms, 200ms, 400ms, etc.
251
- const delay = Math.pow(2, i) * 100;
252
- await new Promise((resolve) => setTimeout(resolve, delay));
253
- }
254
- }
255
- }
256
- throw new errors_1.DeploymentError(`Backend operation failed after ${retries} attempts: ${lastError?.message || 'Unknown error'}`, {
257
- message: `Backend operation failed after ${retries} attempts`,
258
- code: 'BACKEND_OPERATION_FAILED',
259
- details: { originalError: lastError, retries },
260
- });
261
- }
262
- /**
263
- * Log action to audit trail
264
- *
265
- * REQ-O05: Audit log for compliance
266
- *
267
- * @param stackName - Stack name
268
- * @param action - Action performed
269
- * @param state - Current deployment state
270
- * @param error - Error if action failed
271
- */
272
- async logAction(stackName, action, state, error) {
273
- if (!this.options.enableAuditLog) {
274
- return;
275
- }
276
- if (!this.backendProvider.appendAuditLog) {
277
- // Backend doesn't support audit logging yet
278
- return;
279
- }
280
- const entry = {
281
- timestamp: Date.now(),
282
- user: state.user,
283
- action,
284
- stackName,
285
- status: error ? 'failed' : 'completed',
286
- error: error?.message,
287
- changeSet: state.changeSet,
288
- checkpoint: state.checkpoint,
289
- };
290
- try {
291
- await this.backendProvider.appendAuditLog(stackName, entry);
292
- }
293
- catch (err) {
294
- // Don't fail deployment if audit logging fails
295
- logger.error(`Failed to append audit log for ${stackName}:`, err);
296
- }
297
- }
298
- /**
299
- * Save state snapshot for time-travel debugging
300
- *
301
- * Creates a deep copy to ensure immutability.
302
- *
303
- * REQ-O01: Immutable state snapshots
304
- *
305
- * @param stackName - Stack name
306
- * @param state - Current deployment state
307
- */
308
- async saveSnapshot(stackName, state) {
309
- if (!this.options.enableSnapshots) {
310
- return;
311
- }
312
- if (!this.backendProvider.saveSnapshot) {
313
- // Backend doesn't support snapshots yet
314
- return;
315
- }
316
- try {
317
- // Deep copy to ensure immutability
318
- const snapshot = JSON.parse(JSON.stringify({
319
- ...state,
320
- timestamp: Date.now(),
321
- }));
322
- await this.backendProvider.saveSnapshot(stackName, snapshot);
323
- }
324
- catch (err) {
325
- // Don't fail deployment if snapshot fails
326
- logger.error(`Failed to save snapshot for ${stackName}:`, err);
327
- }
328
- }
329
- /**
330
- * Start a deployment transaction
331
- *
332
- * Transitions state from PENDING to APPLYING and saves to backend.
333
- * Acquires lock to prevent concurrent deployments.
334
- *
335
- * REQ-O01.1: WHEN deployment starts THEN CloudDOM state SHALL transition to APPLYING
336
- * REQ-O02: State locking to prevent concurrent deployments
337
- *
338
- * @param stackName - Name of the stack being deployed
339
- * @param changeSet - Change set computed by Reconciler
340
- * @param cloudDOM - CloudDOM tree being deployed
341
- * @param user - User initiating the deployment
342
- * @returns Promise that resolves when state is saved
343
- * @throws DeploymentError if lock cannot be acquired or state save fails
344
- */
345
- async startDeployment(stackName, changeSet, cloudDOM, user) {
346
- // Validate changeSet is not empty
347
- if (!changeSet.deploymentOrder || changeSet.deploymentOrder.length === 0) {
348
- throw new errors_1.DeploymentError(`Cannot start deployment: no resources to deploy for stack ${stackName}`, {
349
- message: `Cannot start deployment: no resources to deploy for stack ${stackName}`,
350
- code: 'EMPTY_CHANGESET',
351
- details: { stackName, changeSet },
352
- });
353
- }
354
- // Acquire lock to prevent concurrent deployments
355
- // NOTE: Lock acquisition should NOT be retried - it should fail immediately
356
- if (this.backendProvider.acquireLock) {
357
- const lockTTL = this.options.lockTTL ?? 600;
358
- try {
359
- await this.backendProvider.acquireLock(stackName, user, lockTTL);
360
- // Start auto-renewal to prevent lock expiration during long deployments
361
- this.startLockRenewal(stackName, user, lockTTL);
362
- }
363
- catch (error) {
364
- throw new errors_1.DeploymentError(`Failed to acquire lock for stack ${stackName}. ` +
365
- `Another deployment may be in progress.`, {
366
- message: `Failed to acquire lock for stack ${stackName}`,
367
- code: 'LOCK_ACQUISITION_FAILED',
368
- details: { stackName, user },
369
- cause: error instanceof Error ? error.message : String(error),
370
- });
371
- }
372
- }
373
- const state = {
374
- status: 'APPLYING',
375
- cloudDOM,
376
- changeSet,
377
- checkpoint: undefined,
378
- timestamp: Date.now(),
379
- user,
380
- stackName,
381
- };
382
- try {
383
- // Validate transition (PENDING → APPLYING)
384
- // Note: We assume PENDING as initial state for new deployments
385
- this.validateTransition('PENDING', 'APPLYING');
386
- // Save state with retry
387
- await this.withRetry(() => this.backendProvider.saveState(stackName, state));
388
- // Log action to audit trail
389
- await this.logAction(stackName, 'start', state);
390
- // Emit event with metadata
391
- this.emit('started', state, {
392
- resourceCount: changeSet.deploymentOrder.length,
393
- creates: changeSet.creates.length,
394
- updates: changeSet.updates.length,
395
- deletes: changeSet.deletes.length,
396
- });
397
- }
398
- catch (error) {
399
- // Stop lock renewal and release lock if state save fails
400
- this.stopLockRenewal(stackName);
401
- if (this.backendProvider.releaseLock) {
402
- try {
403
- await this.backendProvider.releaseLock(stackName);
404
- }
405
- catch (releaseError) {
406
- logger.error(`Failed to release lock after error:`, releaseError);
407
- }
408
- }
409
- throw error;
410
- }
411
- }
412
- /**
413
- * Update checkpoint after each resource deploys
414
- *
415
- * Saves progress to enable crash recovery. If deployment crashes,
416
- * it can resume from the last checkpoint.
417
- *
418
- * REQ-O01.5: WHEN any provider executes THEN StateMachine SHALL checkpoint after each resource
419
- *
420
- * @param stackName - Name of the stack being deployed
421
- * @param checkpoint - Index of last successfully deployed resource
422
- * @returns Promise that resolves when checkpoint is saved
423
- * @throws DeploymentError if state is invalid or save fails
424
- */
425
- async updateCheckpoint(stackName, checkpoint) {
426
- const state = await this.withRetry(() => this.getState(stackName));
427
- if (!state) {
428
- throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
429
- message: `No deployment state found for stack: ${stackName}`,
430
- code: 'STATE_NOT_FOUND',
431
- details: { stackName, checkpoint },
432
- });
433
- }
434
- if (state.status !== 'APPLYING') {
435
- throw new errors_1.DeploymentError(`Cannot update checkpoint for stack in ${state.status} state. ` +
436
- `Checkpoints can only be updated during APPLYING state.`, {
437
- message: `Cannot update checkpoint for stack in ${state.status} state`,
438
- code: 'INVALID_STATE_FOR_CHECKPOINT',
439
- details: { stackName, currentStatus: state.status, checkpoint },
440
- });
441
- }
442
- // Save snapshot before updating checkpoint (for time-travel debugging)
443
- await this.saveSnapshot(stackName, state);
444
- state.checkpoint = checkpoint;
445
- state.timestamp = Date.now();
446
- await this.withRetry(() => this.backendProvider.saveState(stackName, state));
447
- // Log checkpoint to audit trail
448
- await this.logAction(stackName, 'checkpoint', state);
449
- // Emit event
450
- this.emit('checkpoint', state);
451
- }
452
- /**
453
- * Mark deployment as complete
454
- *
455
- * Transitions state from APPLYING to DEPLOYED and releases lock.
456
- *
457
- * REQ-O01.2: WHEN deployment succeeds THEN state SHALL transition to DEPLOYED
458
- * REQ-O02: Release lock after deployment completes
459
- *
460
- * @param stackName - Name of the stack that was deployed
461
- * @returns Promise that resolves when state is saved
462
- * @throws DeploymentError if state is invalid or save fails
463
- */
464
- async completeDeployment(stackName) {
465
- const state = await this.withRetry(() => this.getState(stackName));
466
- if (!state) {
467
- throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
468
- message: `No deployment state found for stack: ${stackName}`,
469
- code: 'STATE_NOT_FOUND',
470
- details: { stackName },
471
- });
472
- }
473
- // Validate transition
474
- this.validateTransition(state.status, 'DEPLOYED');
475
- // Save snapshot before completing
476
- await this.saveSnapshot(stackName, state);
477
- state.status = 'DEPLOYED';
478
- state.timestamp = Date.now();
479
- // Clear checkpoint and changeSet after successful deployment
480
- state.checkpoint = undefined;
481
- state.changeSet = undefined;
482
- await this.withRetry(() => this.backendProvider.saveState(stackName, state));
483
- // Log action to audit trail
484
- await this.logAction(stackName, 'complete', state);
485
- // Stop lock renewal and release lock
486
- this.stopLockRenewal(stackName);
487
- if (this.backendProvider.releaseLock) {
488
- try {
489
- await this.backendProvider.releaseLock(stackName);
490
- }
491
- catch (error) {
492
- logger.error(`Failed to release lock for ${stackName}:`, error);
493
- }
494
- }
495
- // Emit event
496
- this.emit('completed', state);
497
- }
498
- /**
499
- * Mark deployment as failed
500
- *
501
- * Transitions state from APPLYING to FAILED, stores error details, and releases lock.
502
- *
503
- * @param stackName - Name of the stack that failed
504
- * @param error - Error that caused the failure (should be DeploymentError or subclass)
505
- * @returns Promise that resolves when state is saved
506
- * @throws DeploymentError if state is invalid or save fails
507
- */
508
- async failDeployment(stackName, error) {
509
- const state = await this.withRetry(() => this.getState(stackName));
510
- if (!state) {
511
- throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
512
- message: `No deployment state found for stack: ${stackName}`,
513
- code: 'STATE_NOT_FOUND',
514
- details: { stackName },
515
- cause: error.message,
516
- });
517
- }
518
- // Validate transition
519
- this.validateTransition(state.status, 'FAILED');
520
- // Save snapshot before failing
521
- await this.saveSnapshot(stackName, state);
522
- state.status = 'FAILED';
523
- // Store structured error data
524
- if (error instanceof errors_1.DeploymentError) {
525
- state.error = error.data;
526
- }
527
- else {
528
- state.error = {
529
- message: error.message,
530
- stack: error.stack,
531
- code: error.code,
532
- };
533
- }
534
- state.timestamp = Date.now();
535
- await this.withRetry(() => this.backendProvider.saveState(stackName, state));
536
- // Log action to audit trail
537
- await this.logAction(stackName, 'fail', state, error);
538
- // Stop lock renewal and release lock
539
- this.stopLockRenewal(stackName);
540
- if (this.backendProvider.releaseLock) {
541
- try {
542
- await this.backendProvider.releaseLock(stackName);
543
- }
544
- catch (releaseError) {
545
- logger.error(`Failed to release lock for ${stackName}:`, releaseError);
546
- }
547
- }
548
- // Emit event with error metadata
549
- this.emit('failed', state, {
550
- errorCode: state.error?.code,
551
- errorMessage: state.error?.message,
552
- });
553
- }
554
- /**
555
- * Resume interrupted deployment from checkpoint
556
- *
557
- * Returns the change set and checkpoint so deployment can continue
558
- * from where it left off.
559
- *
560
- * REQ-O01.4: WHEN CReact restarts THEN it SHALL detect incomplete transactions
561
- * and offer resume/rollback
562
- *
563
- * @param stackName - Name of the stack to resume
564
- * @returns Promise resolving to change set, checkpoint index, and cloudDOM
565
- * @throws DeploymentError if no incomplete deployment found
566
- */
567
- async resumeDeployment(stackName) {
568
- // Check lock status before resuming
569
- if (this.backendProvider.checkLock) {
570
- const lockInfo = await this.backendProvider.checkLock(stackName);
571
- if (lockInfo) {
572
- const lockAge = Date.now() - lockInfo.acquiredAt;
573
- const lockTTL = lockInfo.ttl * 1000; // Convert to milliseconds
574
- if (lockAge < lockTTL) {
575
- throw new errors_1.DeploymentError(`Cannot resume: lock held by ${lockInfo.holder}`, {
576
- message: `Cannot resume: lock held by ${lockInfo.holder}`,
577
- code: 'LOCK_HELD',
578
- details: {
579
- stackName,
580
- lockHolder: lockInfo.holder,
581
- lockAcquiredAt: new Date(lockInfo.acquiredAt).toISOString(),
582
- lockExpiresAt: new Date(lockInfo.acquiredAt + lockTTL).toISOString(),
583
- },
584
- });
585
- }
586
- }
587
- }
588
- const state = await this.withRetry(() => this.getState(stackName));
589
- if (!state) {
590
- throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
591
- message: `No deployment state found for stack: ${stackName}`,
592
- code: 'STATE_NOT_FOUND',
593
- details: { stackName },
594
- });
595
- }
596
- if (state.status !== 'APPLYING') {
597
- throw new errors_1.DeploymentError(`Cannot resume deployment for stack in ${state.status} state. ` +
598
- `Only APPLYING deployments can be resumed.`, {
599
- message: `Cannot resume deployment for stack in ${state.status} state`,
600
- code: 'INVALID_STATE_FOR_RESUME',
601
- details: { stackName, currentStatus: state.status },
602
- });
603
- }
604
- if (!state.changeSet ||
605
- !state.changeSet.deploymentOrder ||
606
- state.changeSet.deploymentOrder.length === 0) {
607
- throw new errors_1.DeploymentError(`Cannot resume deployment: no change set found in state for stack ${stackName}`, {
608
- message: `Cannot resume deployment: no change set found in state for stack ${stackName}`,
609
- code: 'MISSING_CHANGESET',
610
- details: { stackName },
611
- });
612
- }
613
- return {
614
- changeSet: state.changeSet,
615
- checkpoint: state.checkpoint ?? -1,
616
- cloudDOM: state.cloudDOM,
617
- };
618
- }
619
- /**
620
- * Rollback to previous state
621
- *
622
- * Applies reverse change set (deletes → creates, creates → deletes)
623
- * and transitions state to ROLLED_BACK. Releases lock.
624
- *
625
- * Note: This method only updates the state machine status. The actual
626
- * rollback logic (applying reverse change set) is handled by the caller
627
- * (typically CReact or CLI).
628
- *
629
- * REQ-O01: Crash recovery with rollback
630
- * REQ-O02: Release lock after rollback
631
- *
632
- * @param stackName - Name of the stack to rollback
633
- * @returns Promise that resolves when state is saved
634
- * @throws DeploymentError if state is invalid or save fails
635
- */
636
- async rollback(stackName) {
637
- const state = await this.withRetry(() => this.getState(stackName));
638
- if (!state) {
639
- throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
640
- message: `No deployment state found for stack: ${stackName}`,
641
- code: 'STATE_NOT_FOUND',
642
- details: { stackName },
643
- });
644
- }
645
- // Validate transition
646
- this.validateTransition(state.status, 'ROLLED_BACK');
647
- // Save snapshot before rollback
648
- await this.saveSnapshot(stackName, state);
649
- state.status = 'ROLLED_BACK';
650
- state.timestamp = Date.now();
651
- // Clear checkpoint and changeSet after rollback
652
- state.checkpoint = undefined;
653
- state.changeSet = undefined;
654
- await this.withRetry(() => this.backendProvider.saveState(stackName, state));
655
- // Log action to audit trail
656
- await this.logAction(stackName, 'rollback', state);
657
- // Stop lock renewal and release lock
658
- this.stopLockRenewal(stackName);
659
- if (this.backendProvider.releaseLock) {
660
- try {
661
- await this.backendProvider.releaseLock(stackName);
662
- }
663
- catch (error) {
664
- logger.error(`Failed to release lock for ${stackName}:`, error);
665
- }
666
- }
667
- // Emit event
668
- this.emit('rolled_back', state);
669
- }
670
- /**
671
- * Get current deployment state for a stack
672
- *
673
- * @param stackName - Name of the stack
674
- * @returns Promise resolving to deployment state, or undefined if not found
675
- */
676
- async getState(stackName) {
677
- return await this.backendProvider.getState(stackName);
678
- }
679
- /**
680
- * Update the CloudDOM in the deployment state (for post-deployment effects)
681
- *
682
- * @param stackName - Stack name
683
- * @param cloudDOM - Updated CloudDOM with new outputs
684
- * @throws DeploymentError if state update fails
685
- */
686
- async updateCloudDOM(stackName, cloudDOM) {
687
- const state = await this.withRetry(() => this.getState(stackName));
688
- if (!state) {
689
- throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
690
- message: `No deployment state found for stack: ${stackName}`,
691
- code: 'STATE_NOT_FOUND',
692
- details: { stackName },
693
- });
694
- }
695
- // Update the CloudDOM in the state
696
- state.cloudDOM = cloudDOM;
697
- state.timestamp = Date.now();
698
- // Save the updated state
699
- await this.withRetry(() => this.backendProvider.saveState(stackName, state));
700
- // Log action to audit trail
701
- await this.logAction(stackName, 'checkpoint', state);
702
- }
703
- /**
704
- * Check if a stack has an incomplete deployment
705
- *
706
- * Used on startup to detect crashed deployments that need recovery.
707
- *
708
- * REQ-O01.3: WHEN CReact process crashes mid-deploy THEN state SHALL remain in APPLYING
709
- * REQ-O01.4: WHEN CReact restarts THEN it SHALL detect incomplete transactions
710
- *
711
- * @param stackName - Name of the stack to check
712
- * @returns Promise resolving to true if deployment is incomplete
713
- */
714
- async hasIncompleteDeployment(stackName) {
715
- const state = await this.getState(stackName);
716
- return state?.status === 'APPLYING';
717
- }
718
- /**
719
- * Get checkpoint info for display
720
- *
721
- * Returns human-readable checkpoint information for CLI/UI.
722
- * Includes resource ID for better UX.
723
- *
724
- * @param stackName - Name of the stack
725
- * @returns Promise resolving to checkpoint info, or undefined if not found
726
- */
727
- async getCheckpointInfo(stackName) {
728
- const state = await this.getState(stackName);
729
- if (!state || !state.changeSet) {
730
- return undefined;
731
- }
732
- const checkpoint = state.checkpoint ?? -1;
733
- const totalResources = state.changeSet.deploymentOrder.length;
734
- const percentComplete = totalResources > 0 ? Math.round(((checkpoint + 1) / totalResources) * 100) : 0;
735
- // Get resource ID at checkpoint for better CLI UX
736
- const resourceId = checkpoint >= 0 && checkpoint < state.changeSet.deploymentOrder.length
737
- ? state.changeSet.deploymentOrder[checkpoint]
738
- : undefined;
739
- return {
740
- checkpoint,
741
- resourceId,
742
- totalResources,
743
- percentComplete,
744
- };
745
- }
746
- /**
747
- * Auto-recover from incomplete deployment
748
- *
749
- * Convenience method for CLI/orchestrator to automatically resume or rollback
750
- * based on configuration.
751
- *
752
- * @param stackName - Name of the stack to recover
753
- * @param strategy - Recovery strategy ('resume' or 'rollback')
754
- * @returns Promise resolving to recovery info, or undefined if no recovery needed
755
- */
756
- async autoRecover(stackName, strategy) {
757
- if (!(await this.hasIncompleteDeployment(stackName))) {
758
- return { action: 'none' };
759
- }
760
- if (strategy === 'resume') {
761
- const { changeSet, checkpoint, cloudDOM } = await this.resumeDeployment(stackName);
762
- return {
763
- action: 'resumed',
764
- checkpoint,
765
- changeSet,
766
- cloudDOM,
767
- };
768
- }
769
- else {
770
- await this.rollback(stackName);
771
- return { action: 'rolled_back' };
772
- }
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
- }
870
- }
871
- exports.StateMachine = StateMachine;
872
- /**
873
- * Valid state transitions
874
- *
875
- * Defines allowed transitions for state machine validation.
876
- */
877
- StateMachine.VALID_TRANSITIONS = {
878
- PENDING: ['APPLYING'],
879
- APPLYING: ['DEPLOYED', 'FAILED', 'ROLLED_BACK'],
880
- FAILED: ['ROLLED_BACK'],
881
- DEPLOYED: [],
882
- ROLLED_BACK: [],
883
- };