@creact-labs/creact 0.1.7 → 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 +72 -21
  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 -27
  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
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Render - transform JSX to Fiber tree
3
+ */
4
+ import { popContext, pushContext } from '../primitives/context';
5
+ import { cleanComputation, runComputation } from '../reactive/tracking';
6
+ import { createFiber } from './fiber';
7
+ // Current render context
8
+ let currentFiber = null;
9
+ let currentPath = [];
10
+ // Resource path - only components with useInstance contribute to this
11
+ // This makes wrapper components (without useInstance) transparent
12
+ let resourcePath = [];
13
+ /**
14
+ * Get current fiber (for hooks)
15
+ */
16
+ export function getCurrentFiber() {
17
+ return currentFiber;
18
+ }
19
+ /**
20
+ * Get current path (for hooks)
21
+ */
22
+ export function getCurrentPath() {
23
+ return [...currentPath];
24
+ }
25
+ /**
26
+ * Get current resource path (for useInstance)
27
+ * Only components with useInstance contribute to this path
28
+ */
29
+ export function getCurrentResourcePath() {
30
+ return [...resourcePath];
31
+ }
32
+ /**
33
+ * Push a segment to resource path (called by useInstance)
34
+ */
35
+ export function pushResourcePath(segment) {
36
+ resourcePath.push(segment);
37
+ }
38
+ /**
39
+ * Pop a segment from resource path (called after component with instance renders)
40
+ */
41
+ export function popResourcePath() {
42
+ resourcePath.pop();
43
+ }
44
+ /**
45
+ * Reset resource path (called at start of render)
46
+ */
47
+ export function resetResourcePath() {
48
+ resourcePath = [];
49
+ }
50
+ /**
51
+ * Render a JSX element to a Fiber
52
+ */
53
+ export function renderFiber(element, path) {
54
+ // Handle null/undefined/boolean
55
+ if (element == null || typeof element === 'boolean') {
56
+ return createFiber(null, {}, path);
57
+ }
58
+ // Handle primitives (text)
59
+ if (typeof element === 'string' || typeof element === 'number') {
60
+ return createFiber('text', { value: element }, path);
61
+ }
62
+ // Handle arrays
63
+ if (Array.isArray(element)) {
64
+ const fiber = createFiber('fragment', {}, path);
65
+ fiber.children = element.map((child, i) => {
66
+ const childKey = child?.key ?? i;
67
+ return renderFiber(child, [...path, String(childKey)]);
68
+ });
69
+ return fiber;
70
+ }
71
+ // Handle JSX element
72
+ const { type, props = {}, key } = element;
73
+ const { children, ...restProps } = props;
74
+ // Generate path segment
75
+ const name = getNodeName(type, props, key);
76
+ const fiberPath = [...path, name];
77
+ const fiber = createFiber(type, restProps, fiberPath, key);
78
+ // Check if this is a context provider
79
+ if (element.__isProvider && element.__context) {
80
+ pushContext(element.__context, props.value);
81
+ try {
82
+ fiber.children = renderChildren(children, fiberPath);
83
+ }
84
+ finally {
85
+ popContext(element.__context);
86
+ }
87
+ return fiber;
88
+ }
89
+ // Handle function components
90
+ if (typeof type === 'function') {
91
+ // Capture incoming resource path for reactive re-renders
92
+ fiber.incomingResourcePath = [...resourcePath];
93
+ // Create computation for this component
94
+ const computation = {
95
+ fn: () => executeComponent(fiber, type, { ...restProps, children }),
96
+ sources: null,
97
+ sourceSlots: null,
98
+ state: 1, // STALE
99
+ cleanups: null,
100
+ };
101
+ fiber.computation = computation;
102
+ // Initial render
103
+ runComputation(computation);
104
+ }
105
+ else {
106
+ // Intrinsic element - just render children
107
+ fiber.children = renderChildren(children, fiberPath);
108
+ }
109
+ return fiber;
110
+ }
111
+ /**
112
+ * Recursively clean up computations from a fiber tree
113
+ * This prevents stale computations from observing signals after re-render
114
+ * and ensures queued computations won't run (by marking them CLEAN)
115
+ */
116
+ function cleanupFiberTree(fibers) {
117
+ for (const fiber of fibers) {
118
+ if (fiber.computation) {
119
+ cleanComputation(fiber.computation);
120
+ // Mark as CLEAN (0) so it won't run if already queued in batch
121
+ fiber.computation.state = 0;
122
+ }
123
+ if (fiber.children.length > 0) {
124
+ cleanupFiberTree(fiber.children);
125
+ }
126
+ }
127
+ }
128
+ /**
129
+ * Execute a component function
130
+ */
131
+ // biome-ignore lint/complexity/noBannedTypes: JSX component types are dynamically resolved at runtime
132
+ function executeComponent(fiber, type, props) {
133
+ const prevFiber = currentFiber;
134
+ const prevPath = currentPath;
135
+ currentFiber = fiber;
136
+ currentPath = fiber.path;
137
+ // Restore resource path for reactive re-renders
138
+ // This ensures components see the correct resource path even when
139
+ // re-executing independently (not from root)
140
+ resourcePath = [...(fiber.incomingResourcePath ?? [])];
141
+ // Clean up old children's computations before re-rendering
142
+ // This prevents stale computations from observing signals
143
+ if (fiber.children.length > 0) {
144
+ cleanupFiberTree(fiber.children);
145
+ }
146
+ // Clear instance nodes and placeholder flag before re-executing component
147
+ fiber.instanceNodes = [];
148
+ fiber.hasPlaceholderInstance = false;
149
+ try {
150
+ // Execute component
151
+ const result = type(props);
152
+ // Render children from result
153
+ fiber.children = renderChildren(result, fiber.path);
154
+ }
155
+ finally {
156
+ // If this component had useInstance (real or placeholder), pop its resource path segment
157
+ // (useInstance pushes a segment so children see it in their resource path)
158
+ if (fiber.instanceNodes.length > 0 || fiber.hasPlaceholderInstance) {
159
+ popResourcePath();
160
+ }
161
+ currentFiber = prevFiber;
162
+ currentPath = prevPath;
163
+ }
164
+ }
165
+ /**
166
+ * Render children (handles various child types)
167
+ */
168
+ function renderChildren(children, parentPath) {
169
+ if (children == null || typeof children === 'boolean') {
170
+ return [];
171
+ }
172
+ if (Array.isArray(children)) {
173
+ return children.flatMap((child, i) => {
174
+ const childKey = child?.key ?? i;
175
+ const fiber = renderFiber(child, [...parentPath, String(childKey)]);
176
+ return fiber.type === null ? [] : [fiber];
177
+ });
178
+ }
179
+ const fiber = renderFiber(children, parentPath);
180
+ return fiber.type === null ? [] : [fiber];
181
+ }
182
+ /**
183
+ * Get node name from type and props
184
+ */
185
+ function getNodeName(type, _props, key) {
186
+ if (key !== undefined) {
187
+ return String(key);
188
+ }
189
+ if (typeof type === 'string') {
190
+ return type;
191
+ }
192
+ if (typeof type === 'function') {
193
+ return type.name || 'Component';
194
+ }
195
+ return 'unknown';
196
+ }
197
+ /**
198
+ * Collect all instance nodes from fiber tree
199
+ * Returns cloned nodes to ensure returned arrays are independent snapshots
200
+ */
201
+ export function collectInstanceNodes(fiber) {
202
+ const nodes = [];
203
+ function walk(f) {
204
+ for (const instanceNode of f.instanceNodes) {
205
+ // Clone the node to create an independent snapshot
206
+ // (outputSignals are not cloned - they're internal reactive state)
207
+ const snapshot = {
208
+ ...instanceNode,
209
+ // Deep clone the store so it's independent between runs
210
+ store: f.store ? JSON.parse(JSON.stringify(f.store)) : undefined,
211
+ };
212
+ nodes.push(snapshot);
213
+ }
214
+ for (const child of f.children) {
215
+ walk(child);
216
+ }
217
+ }
218
+ walk(fiber);
219
+ return nodes;
220
+ }
221
+ /**
222
+ * Clean up a fiber tree
223
+ */
224
+ export function cleanupFiber(fiber) {
225
+ if (fiber.computation) {
226
+ cleanComputation(fiber.computation);
227
+ }
228
+ for (const child of fiber.children) {
229
+ cleanupFiber(child);
230
+ }
231
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * CReact - Continuous Reactive Runtime with State Machine
3
+ *
4
+ * Event-driven runtime that:
5
+ * 1. Renders JSX -> Fiber tree
6
+ * 2. Reconciles and applies changes with dependency ordering
7
+ * 3. Persists state to backend for crash recovery
8
+ * 4. Subscribes to provider events
9
+ * 5. When events arrive -> updates affected fibers -> re-renders -> applies new changes
10
+ */
11
+ import type { InstanceNode } from '../primitives/instance';
12
+ import type { Backend } from '../provider/backend';
13
+ import type { OutputChangeEvent, Provider } from '../provider/interface';
14
+ import type { Fiber } from './fiber';
15
+ import { StateMachine } from './state-machine';
16
+ export interface CReactOptions {
17
+ maxIterations?: number;
18
+ /** User identifier for audit logs */
19
+ user?: string;
20
+ /** Enable audit logging (requires backend support) */
21
+ enableAuditLog?: boolean;
22
+ }
23
+ /**
24
+ * CReact - Continuous reactive runtime with state machine
25
+ *
26
+ * Subscribes to provider events and automatically re-renders
27
+ * components when outputs change. This is event-driven, not poll-driven.
28
+ *
29
+ * Requires both:
30
+ * - CReact.provider: Materializes resources (AWS, Terraform, etc.)
31
+ * - CReact.backend: Persists deployment state (file, DynamoDB, etc.)
32
+ */
33
+ export declare class CReact {
34
+ static provider: Provider;
35
+ static backend: Backend;
36
+ private static _lastRuntime;
37
+ private static _lastStackName;
38
+ protected instanceProvider: Provider;
39
+ protected instanceBackend: Backend;
40
+ protected stateMachine: StateMachine;
41
+ protected rootFiber: Fiber | null;
42
+ protected currentNodes: InstanceNode[];
43
+ protected options: CReactOptions;
44
+ protected outputChangeHandler: ((change: OutputChangeEvent) => void) | null;
45
+ protected stackName: string | null;
46
+ constructor(provider: Provider, backend: Backend, options?: CReactOptions);
47
+ /**
48
+ * Start the runtime - render, apply, and begin listening for events
49
+ *
50
+ * @param element - JSX element to render
51
+ * @param stackNameOrPrevious - Stack name (string) or previous nodes (array) for backward compat
52
+ */
53
+ run(element: any, stackNameOrPrevious?: string | InstanceNode[]): Promise<void>;
54
+ /**
55
+ * Apply changes between previous and current nodes
56
+ *
57
+ * Uses dependency ordering from reconciler:
58
+ * 1. Apply deletes (reverse dependency order)
59
+ * 2. Apply creates/updates in deployment order
60
+ * 3. Recursively apply any new nodes that appeared
61
+ *
62
+ * With synchronous batching, fillInstanceOutputs triggers dependent
63
+ * re-renders immediately. New nodes appear in registry right away.
64
+ */
65
+ protected applyChanges(previousNodes: InstanceNode[]): Promise<void>;
66
+ /**
67
+ * Handle provider output change events
68
+ *
69
+ * This is the event-driven part - when provider emits changes,
70
+ * we update signals (triggering synchronous re-renders via batch),
71
+ * and apply any new nodes.
72
+ */
73
+ protected handleOutputChange(change: OutputChangeEvent): Promise<void>;
74
+ /**
75
+ * Get current instance nodes (for debugging/testing)
76
+ */
77
+ getNodes(): InstanceNode[];
78
+ /**
79
+ * Get the state machine (for advanced usage)
80
+ */
81
+ getStateMachine(): StateMachine | null;
82
+ /**
83
+ * Stop the runtime
84
+ */
85
+ stop(): void;
86
+ /**
87
+ * Render CloudDOM - main entry point
88
+ *
89
+ * Usage:
90
+ * CReact.provider = myProvider;
91
+ * CReact.backend = myBackend;
92
+ * export default () => renderCloudDOM(<App />, 'my-stack');
93
+ */
94
+ static renderCloudDOM(element: any, stackName: string): Promise<InstanceNode[]>;
95
+ /**
96
+ * Get last runtime instance (for hot reload / CLI tooling)
97
+ * Hot reload uses this to re-render with updated code without full restart
98
+ */
99
+ static getLastRuntime(): {
100
+ runtime: CReact;
101
+ stackName: string;
102
+ } | null;
103
+ }
104
+ /**
105
+ * Reset all runtime state (for testing)
106
+ */
107
+ export declare function resetRuntime(): void;
108
+ /**
109
+ * Convenience function to run once and return nodes (for testing)
110
+ *
111
+ * Creates a runtime with a simple in-memory backend and runs it.
112
+ * Use this for tests where you don't need persistent state.
113
+ */
114
+ export declare function run(rootElement: any, provider: Provider, previousNodes?: InstanceNode[], options?: CReactOptions): Promise<InstanceNode[]>;
115
+ /**
116
+ * Run with explicit backend (for production use)
117
+ */
118
+ export declare function runWithBackend(rootElement: any, provider: Provider, backend: Backend, stackName: string, options?: CReactOptions): Promise<InstanceNode[]>;
119
+ export declare const renderCloudDOM: typeof CReact.renderCloudDOM;
@@ -0,0 +1,334 @@
1
+ /**
2
+ * CReact - Continuous Reactive Runtime with State Machine
3
+ *
4
+ * Event-driven runtime that:
5
+ * 1. Renders JSX -> Fiber tree
6
+ * 2. Reconciles and applies changes with dependency ordering
7
+ * 3. Persists state to backend for crash recovery
8
+ * 4. Subscribes to provider events
9
+ * 5. When events arrive -> updates affected fibers -> re-renders -> applies new changes
10
+ */
11
+ import { clearContextStacks } from '../primitives/context';
12
+ import { clearNodeOwnership, clearNodeRegistry, clearOutputHydration, fillInstanceOutputs, getNodeById, prepareOutputHydration, } from '../primitives/instance';
13
+ import { clearHydration, prepareHydration } from '../primitives/store';
14
+ import { serializeNodes } from '../provider/backend';
15
+ import { buildDependencyGraph, reconcile, topologicalSort } from './reconcile';
16
+ import { collectInstanceNodes, renderFiber, resetResourcePath } from './render';
17
+ // flushSync no longer needed - batch() flushes synchronously
18
+ import { StateMachine } from './state-machine';
19
+ /**
20
+ * CReact - Continuous reactive runtime with state machine
21
+ *
22
+ * Subscribes to provider events and automatically re-renders
23
+ * components when outputs change. This is event-driven, not poll-driven.
24
+ *
25
+ * Requires both:
26
+ * - CReact.provider: Materializes resources (AWS, Terraform, etc.)
27
+ * - CReact.backend: Persists deployment state (file, DynamoDB, etc.)
28
+ */
29
+ export class CReact {
30
+ // Singleton configuration (both required)
31
+ static provider;
32
+ static backend;
33
+ // Internal storage for last runtime (for hot reload / CLI tooling)
34
+ static _lastRuntime = null;
35
+ static _lastStackName = null;
36
+ instanceProvider;
37
+ instanceBackend;
38
+ stateMachine;
39
+ rootFiber = null;
40
+ currentNodes = [];
41
+ options;
42
+ outputChangeHandler = null;
43
+ stackName = null;
44
+ constructor(provider, backend, options = {}) {
45
+ this.instanceProvider = provider;
46
+ this.instanceBackend = backend;
47
+ this.options = { maxIterations: 10, ...options };
48
+ // Initialize state machine (always available now)
49
+ this.stateMachine = new StateMachine(backend, {
50
+ user: options.user,
51
+ enableAuditLog: options.enableAuditLog,
52
+ });
53
+ // Subscribe to provider events immediately
54
+ if (provider.on) {
55
+ this.outputChangeHandler = (change) => this.handleOutputChange(change);
56
+ provider.on('outputsChanged', this.outputChangeHandler);
57
+ }
58
+ }
59
+ /**
60
+ * Start the runtime - render, apply, and begin listening for events
61
+ *
62
+ * @param element - JSX element to render
63
+ * @param stackNameOrPrevious - Stack name (string) or previous nodes (array) for backward compat
64
+ */
65
+ async run(element, stackNameOrPrevious) {
66
+ // Handle backward compatibility: string = stackName, array = previousNodes
67
+ let previousNodes;
68
+ if (typeof stackNameOrPrevious === 'string') {
69
+ this.stackName = stackNameOrPrevious;
70
+ }
71
+ else if (Array.isArray(stackNameOrPrevious)) {
72
+ previousNodes = stackNameOrPrevious;
73
+ }
74
+ // Check for interrupted deployment - just log it, normal reconciliation handles resume
75
+ // Nodes with outputs are treated as "same", nodes without are (re)created
76
+ if (this.stackName && (await this.stateMachine.canResume(this.stackName))) {
77
+ const interruptedNode = await this.stateMachine.getInterruptedNodeId(this.stackName);
78
+ console.log(`[CReact] Resuming interrupted deployment${interruptedNode ? ` (interrupted at: ${interruptedNode})` : ''}`);
79
+ }
80
+ // Load previous state from backend if available
81
+ if (this.instanceBackend && this.stackName && !previousNodes) {
82
+ const prevState = await this.stateMachine?.getPreviousState(this.stackName);
83
+ if (prevState) {
84
+ // Restore resource states
85
+ this.stateMachine?.restoreResourceStates(prevState.nodes);
86
+ // Prepare hydration for createStore
87
+ prepareHydration(prevState.nodes);
88
+ // Prepare output hydration for useInstance
89
+ prepareOutputHydration(prevState.nodes);
90
+ // Use previous nodes for reconciliation
91
+ // Filter to nodes WITH outputs (completed deployment)
92
+ // Nodes without outputs (crashed mid-deploy) will be treated as "creates"
93
+ previousNodes = prevState.nodes.filter((n) => n.outputs && Object.keys(n.outputs).length > 0);
94
+ }
95
+ }
96
+ else if (previousNodes) {
97
+ prepareHydration(previousNodes);
98
+ prepareOutputHydration(previousNodes);
99
+ }
100
+ // Clear ownership for new render pass (allows re-renders with same node IDs)
101
+ clearNodeOwnership();
102
+ // Initial render
103
+ this.rootFiber = renderFiber(element, []);
104
+ this.currentNodes = collectInstanceNodes(this.rootFiber);
105
+ // Clear hydration maps after render (no longer needed)
106
+ clearOutputHydration();
107
+ // Initial apply cycle
108
+ await this.applyChanges(previousNodes ?? []);
109
+ // Runtime is now active - events will trigger handleOutputChange
110
+ // For tests: return immediately (events still work)
111
+ // For CLI: caller can await a keep-alive promise if needed
112
+ }
113
+ /**
114
+ * Apply changes between previous and current nodes
115
+ *
116
+ * Uses dependency ordering from reconciler:
117
+ * 1. Apply deletes (reverse dependency order)
118
+ * 2. Apply creates/updates in deployment order
119
+ * 3. Recursively apply any new nodes that appeared
120
+ *
121
+ * With synchronous batching, fillInstanceOutputs triggers dependent
122
+ * re-renders immediately. New nodes appear in registry right away.
123
+ */
124
+ async applyChanges(previousNodes) {
125
+ const changes = reconcile(previousNodes, this.currentNodes);
126
+ if (changes.creates.length === 0 &&
127
+ changes.updates.length === 0 &&
128
+ changes.deletes.length === 0) {
129
+ // No changes - update state machine to reflect stable state
130
+ if (this.stackName) {
131
+ await this.stateMachine.completeDeployment(this.stackName, serializeNodes(this.currentNodes));
132
+ }
133
+ return;
134
+ }
135
+ // Start deployment tracking
136
+ if (this.stackName) {
137
+ await this.stateMachine.startDeployment(this.stackName, serializeNodes(this.currentNodes), {
138
+ creates: changes.creates.length,
139
+ updates: changes.updates.length,
140
+ deletes: changes.deletes.length,
141
+ });
142
+ }
143
+ try {
144
+ // Apply deletes (reverse order - children first)
145
+ const deleteIds = changes.deletes.map((n) => n.id);
146
+ const graph = buildDependencyGraph(changes.deletes);
147
+ const deleteOrder = topologicalSort(deleteIds, graph).reverse();
148
+ for (const nodeId of deleteOrder) {
149
+ const node = changes.deletes.find((n) => n.id === nodeId);
150
+ if (node) {
151
+ this.stateMachine.setResourceState(nodeId, 'applying');
152
+ await this.instanceProvider.destroy(node);
153
+ if (this.stackName) {
154
+ await this.stateMachine.recordResourceDestroyed(this.stackName, nodeId);
155
+ }
156
+ }
157
+ }
158
+ // Materialize creates and updates in deployment order
159
+ // With synchronous batching, fillInstanceOutputs triggers re-renders
160
+ // immediately - new nodes appear in registry right after each materialize
161
+ for (const nodeId of changes.deploymentOrder) {
162
+ // Get current node state from registry (reflects signal updates from prior materializes)
163
+ const node = getNodeById(nodeId);
164
+ if (!node)
165
+ continue;
166
+ this.stateMachine.setResourceState(nodeId, 'applying');
167
+ // Mark this node as being applied (for crash recovery)
168
+ if (this.stackName) {
169
+ await this.stateMachine.markApplying(this.stackName, nodeId);
170
+ }
171
+ // Materialize - provider calls node.setOutputs() when outputs are available
172
+ // For sync providers: setOutputs() is called immediately, triggers reactive updates
173
+ // For async providers: setOutputs() is called in callback, triggers reactive updates later
174
+ await this.instanceProvider.materialize([node]);
175
+ // Extract outputs for state recording (may be empty for async providers)
176
+ const outputs = node.outputs ?? {};
177
+ // Note: fillInstanceOutputs is no longer called here - provider uses node.setOutputs()
178
+ this.stateMachine.setResourceState(nodeId, 'deployed');
179
+ if (this.stackName) {
180
+ await this.stateMachine.clearApplying(this.stackName);
181
+ await this.stateMachine.updateNodeOutputs(this.stackName, nodeId, outputs);
182
+ await this.stateMachine.recordResourceApplied(this.stackName, nodeId, outputs);
183
+ }
184
+ }
185
+ // Re-collect nodes - new nodes are already created thanks to synchronous batching
186
+ const newNodes = collectInstanceNodes(this.rootFiber);
187
+ const hasNewNodes = newNodes.some((n) => !this.currentNodes.some((p) => p.id === n.id));
188
+ if (hasNewNodes) {
189
+ const prevNodes = this.currentNodes;
190
+ this.currentNodes = newNodes;
191
+ await this.applyChanges(prevNodes); // Recurse for new nodes
192
+ }
193
+ else {
194
+ this.currentNodes = newNodes;
195
+ // Complete deployment
196
+ if (this.stackName) {
197
+ await this.stateMachine.completeDeployment(this.stackName, serializeNodes(this.currentNodes));
198
+ }
199
+ }
200
+ }
201
+ catch (error) {
202
+ // Record failure
203
+ if (this.stackName) {
204
+ await this.stateMachine.failDeployment(this.stackName, error instanceof Error ? error : new Error(String(error)));
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+ /**
210
+ * Handle provider output change events
211
+ *
212
+ * This is the event-driven part - when provider emits changes,
213
+ * we update signals (triggering synchronous re-renders via batch),
214
+ * and apply any new nodes.
215
+ */
216
+ async handleOutputChange(change) {
217
+ // Find node by resource name (props.name) or node.id
218
+ const node = this.currentNodes.find((n) => n.props.name === change.resourceName || n.id === change.resourceName);
219
+ if (!node) {
220
+ console.warn(`[CReact] No node found for resource: ${change.resourceName}`);
221
+ return;
222
+ }
223
+ // Update signal - reactive system triggers component re-execution
224
+ // With synchronous batching, re-renders happen inside fillInstanceOutputs
225
+ fillInstanceOutputs(node.id, change.outputs);
226
+ // Re-collect and apply any new nodes
227
+ if (this.rootFiber) {
228
+ const prevNodes = this.currentNodes;
229
+ this.currentNodes = collectInstanceNodes(this.rootFiber);
230
+ await this.applyChanges(prevNodes);
231
+ }
232
+ }
233
+ /**
234
+ * Get current instance nodes (for debugging/testing)
235
+ */
236
+ getNodes() {
237
+ return [...this.currentNodes];
238
+ }
239
+ /**
240
+ * Get the state machine (for advanced usage)
241
+ */
242
+ getStateMachine() {
243
+ return this.stateMachine;
244
+ }
245
+ /**
246
+ * Stop the runtime
247
+ */
248
+ stop() {
249
+ if (this.instanceProvider.off && this.outputChangeHandler) {
250
+ this.instanceProvider.off('outputsChanged', this.outputChangeHandler);
251
+ }
252
+ if (this.instanceProvider.stop) {
253
+ this.instanceProvider.stop();
254
+ }
255
+ }
256
+ /**
257
+ * Render CloudDOM - main entry point
258
+ *
259
+ * Usage:
260
+ * CReact.provider = myProvider;
261
+ * CReact.backend = myBackend;
262
+ * export default () => renderCloudDOM(<App />, 'my-stack');
263
+ */
264
+ static async renderCloudDOM(element, stackName) {
265
+ if (!CReact.provider) {
266
+ throw new Error('CReact.provider must be set before calling renderCloudDOM');
267
+ }
268
+ if (!CReact.backend) {
269
+ throw new Error('CReact.backend must be set before calling renderCloudDOM');
270
+ }
271
+ const runtime = new CReact(CReact.provider, CReact.backend);
272
+ await runtime.run(element, stackName);
273
+ // Store for CLI/tooling access
274
+ CReact._lastRuntime = runtime;
275
+ CReact._lastStackName = stackName;
276
+ // Return immediately - process stays alive via provider event listeners
277
+ return runtime.getNodes();
278
+ }
279
+ /**
280
+ * Get last runtime instance (for hot reload / CLI tooling)
281
+ * Hot reload uses this to re-render with updated code without full restart
282
+ */
283
+ static getLastRuntime() {
284
+ const runtime = CReact._lastRuntime;
285
+ const stackName = CReact._lastStackName;
286
+ return runtime && stackName ? { runtime, stackName } : null;
287
+ }
288
+ }
289
+ /**
290
+ * Reset all runtime state (for testing)
291
+ */
292
+ export function resetRuntime() {
293
+ clearNodeRegistry();
294
+ clearHydration();
295
+ clearOutputHydration();
296
+ clearContextStacks();
297
+ resetResourcePath();
298
+ }
299
+ /**
300
+ * Minimal in-memory backend for the convenience run() function
301
+ * @internal
302
+ */
303
+ class SimpleInMemoryBackend {
304
+ states = new Map();
305
+ async getState(stackName) {
306
+ const state = this.states.get(stackName);
307
+ return state ? JSON.parse(JSON.stringify(state)) : null;
308
+ }
309
+ async saveState(stackName, state) {
310
+ this.states.set(stackName, JSON.parse(JSON.stringify(state)));
311
+ }
312
+ }
313
+ /**
314
+ * Convenience function to run once and return nodes (for testing)
315
+ *
316
+ * Creates a runtime with a simple in-memory backend and runs it.
317
+ * Use this for tests where you don't need persistent state.
318
+ */
319
+ export async function run(rootElement, provider, previousNodes, options) {
320
+ const backend = new SimpleInMemoryBackend();
321
+ const runtime = new CReact(provider, backend, options);
322
+ await runtime.run(rootElement, previousNodes);
323
+ return runtime.getNodes();
324
+ }
325
+ /**
326
+ * Run with explicit backend (for production use)
327
+ */
328
+ export async function runWithBackend(rootElement, provider, backend, stackName, options) {
329
+ const runtime = new CReact(provider, backend, options);
330
+ await runtime.run(rootElement, stackName);
331
+ return runtime.getNodes();
332
+ }
333
+ // Convenience export
334
+ export const renderCloudDOM = CReact.renderCloudDOM.bind(CReact);