@creact-labs/creact 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -22
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +88 -0
- package/dist/index.d.ts +19 -44
- package/dist/index.js +20 -68
- package/dist/jsx/index.d.ts +2 -0
- package/dist/jsx/index.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +4 -0
- package/dist/jsx/jsx-dev-runtime.js +4 -0
- package/dist/jsx/jsx-runtime.d.ts +38 -0
- package/dist/jsx/jsx-runtime.js +38 -0
- package/dist/jsx/types.d.ts +12 -0
- package/dist/jsx/types.js +4 -0
- package/dist/primitives/context.d.ts +34 -0
- package/dist/primitives/context.js +63 -0
- package/dist/primitives/index.d.ts +3 -0
- package/dist/primitives/index.js +3 -0
- package/dist/primitives/instance.d.ts +72 -0
- package/dist/primitives/instance.js +235 -0
- package/dist/primitives/store.d.ts +22 -0
- package/dist/primitives/store.js +97 -0
- package/dist/provider/backend.d.ts +110 -0
- package/dist/provider/backend.js +37 -0
- package/dist/provider/interface.d.ts +48 -0
- package/dist/provider/interface.js +39 -0
- package/dist/reactive/effect.d.ts +11 -0
- package/dist/reactive/effect.js +42 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +3 -0
- package/dist/reactive/signal.d.ts +32 -0
- package/dist/reactive/signal.js +60 -0
- package/dist/reactive/tracking.d.ts +41 -0
- package/dist/reactive/tracking.js +161 -0
- package/dist/runtime/fiber.d.ts +21 -0
- package/dist/runtime/fiber.js +16 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/reconcile.d.ts +66 -0
- package/dist/runtime/reconcile.js +210 -0
- package/dist/runtime/render.d.ts +42 -0
- package/dist/runtime/render.js +231 -0
- package/dist/runtime/run.d.ts +119 -0
- package/dist/runtime/run.js +334 -0
- package/dist/runtime/state-machine.d.ts +95 -0
- package/dist/runtime/state-machine.js +209 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +4 -0
- package/package.json +29 -24
- package/dist/cli/commands/BuildCommand.d.ts +0 -40
- package/dist/cli/commands/BuildCommand.js +0 -151
- package/dist/cli/commands/DeployCommand.d.ts +0 -38
- package/dist/cli/commands/DeployCommand.js +0 -194
- package/dist/cli/commands/DevCommand.d.ts +0 -52
- package/dist/cli/commands/DevCommand.js +0 -394
- package/dist/cli/commands/PlanCommand.d.ts +0 -39
- package/dist/cli/commands/PlanCommand.js +0 -164
- package/dist/cli/commands/index.d.ts +0 -36
- package/dist/cli/commands/index.js +0 -43
- package/dist/cli/core/ArgumentParser.d.ts +0 -46
- package/dist/cli/core/ArgumentParser.js +0 -127
- package/dist/cli/core/BaseCommand.d.ts +0 -75
- package/dist/cli/core/BaseCommand.js +0 -95
- package/dist/cli/core/CLIContext.d.ts +0 -68
- package/dist/cli/core/CLIContext.js +0 -183
- package/dist/cli/core/CommandRegistry.d.ts +0 -64
- package/dist/cli/core/CommandRegistry.js +0 -89
- package/dist/cli/core/index.d.ts +0 -36
- package/dist/cli/core/index.js +0 -43
- package/dist/cli/index.d.ts +0 -35
- package/dist/cli/index.js +0 -100
- package/dist/cli/output.d.ts +0 -204
- package/dist/cli/output.js +0 -437
- package/dist/cli/utils.d.ts +0 -59
- package/dist/cli/utils.js +0 -76
- package/dist/context/createContext.d.ts +0 -90
- package/dist/context/createContext.js +0 -113
- package/dist/context/index.d.ts +0 -30
- package/dist/context/index.js +0 -35
- package/dist/core/CReact.d.ts +0 -409
- package/dist/core/CReact.js +0 -1151
- package/dist/core/CloudDOMBuilder.d.ts +0 -447
- package/dist/core/CloudDOMBuilder.js +0 -1234
- package/dist/core/ContextDependencyTracker.d.ts +0 -165
- package/dist/core/ContextDependencyTracker.js +0 -448
- package/dist/core/ErrorRecoveryManager.d.ts +0 -145
- package/dist/core/ErrorRecoveryManager.js +0 -443
- package/dist/core/EventBus.d.ts +0 -91
- package/dist/core/EventBus.js +0 -185
- package/dist/core/ProviderOutputTracker.d.ts +0 -211
- package/dist/core/ProviderOutputTracker.js +0 -476
- package/dist/core/ReactiveUpdateQueue.d.ts +0 -76
- package/dist/core/ReactiveUpdateQueue.js +0 -121
- package/dist/core/Reconciler.d.ts +0 -415
- package/dist/core/Reconciler.js +0 -1044
- package/dist/core/RenderScheduler.d.ts +0 -153
- package/dist/core/RenderScheduler.js +0 -519
- package/dist/core/Renderer.d.ts +0 -336
- package/dist/core/Renderer.js +0 -944
- package/dist/core/Runtime.d.ts +0 -246
- package/dist/core/Runtime.js +0 -640
- package/dist/core/StateBindingManager.d.ts +0 -121
- package/dist/core/StateBindingManager.js +0 -309
- package/dist/core/StateMachine.d.ts +0 -441
- package/dist/core/StateMachine.js +0 -883
- package/dist/core/StructuralChangeDetector.d.ts +0 -140
- package/dist/core/StructuralChangeDetector.js +0 -363
- package/dist/core/Validator.d.ts +0 -127
- package/dist/core/Validator.js +0 -279
- package/dist/core/errors.d.ts +0 -153
- package/dist/core/errors.js +0 -202
- package/dist/core/index.d.ts +0 -38
- package/dist/core/index.js +0 -64
- package/dist/core/types.d.ts +0 -265
- package/dist/core/types.js +0 -48
- package/dist/hooks/context.d.ts +0 -147
- package/dist/hooks/context.js +0 -334
- package/dist/hooks/useContext.d.ts +0 -113
- package/dist/hooks/useContext.js +0 -169
- package/dist/hooks/useEffect.d.ts +0 -105
- package/dist/hooks/useEffect.js +0 -540
- package/dist/hooks/useInstance.d.ts +0 -139
- package/dist/hooks/useInstance.js +0 -455
- package/dist/hooks/useState.d.ts +0 -120
- package/dist/hooks/useState.js +0 -298
- package/dist/jsx.d.ts +0 -143
- package/dist/jsx.js +0 -76
- package/dist/providers/DummyBackendProvider.d.ts +0 -193
- package/dist/providers/DummyBackendProvider.js +0 -189
- package/dist/providers/DummyCloudProvider.d.ts +0 -128
- package/dist/providers/DummyCloudProvider.js +0 -157
- package/dist/providers/IBackendProvider.d.ts +0 -177
- package/dist/providers/IBackendProvider.js +0 -31
- package/dist/providers/ICloudProvider.d.ts +0 -230
- package/dist/providers/ICloudProvider.js +0 -31
- package/dist/providers/index.d.ts +0 -31
- package/dist/providers/index.js +0 -31
- package/dist/test-event-callbacks.d.ts +0 -0
- package/dist/test-event-callbacks.js +0 -1
- package/dist/utils/Logger.d.ts +0 -144
- package/dist/utils/Logger.js +0 -220
- package/dist/utils/Output.d.ts +0 -161
- package/dist/utils/Output.js +0 -401
- package/dist/utils/deepEqual.d.ts +0 -71
- package/dist/utils/deepEqual.js +0 -276
- package/dist/utils/naming.d.ts +0 -241
- package/dist/utils/naming.js +0 -376
|
@@ -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);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateMachine - Resource lifecycle tracking and deployment orchestration
|
|
3
|
+
*
|
|
4
|
+
* Tracks:
|
|
5
|
+
* - Individual resource states (pending → applying → deployed)
|
|
6
|
+
* - Deployment lifecycle (start → checkpoint → complete/fail)
|
|
7
|
+
* - Resume capability from crash
|
|
8
|
+
*/
|
|
9
|
+
import type { Backend, DeploymentState, ResourceState, SerializedNode } from '../provider/backend';
|
|
10
|
+
export interface StateMachineOptions {
|
|
11
|
+
user?: string;
|
|
12
|
+
enableAuditLog?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* StateMachine manages deployment lifecycle and resource states
|
|
16
|
+
*/
|
|
17
|
+
export declare class StateMachine {
|
|
18
|
+
private backend;
|
|
19
|
+
private resourceStates;
|
|
20
|
+
private options;
|
|
21
|
+
constructor(backend: Backend, options?: StateMachineOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Get the current state of a resource
|
|
24
|
+
*/
|
|
25
|
+
getResourceState(nodeId: string): ResourceState;
|
|
26
|
+
/**
|
|
27
|
+
* Set the state of a resource
|
|
28
|
+
*/
|
|
29
|
+
setResourceState(nodeId: string, state: ResourceState): void;
|
|
30
|
+
/**
|
|
31
|
+
* Restore resource states from persisted nodes
|
|
32
|
+
*/
|
|
33
|
+
restoreResourceStates(nodes: SerializedNode[]): void;
|
|
34
|
+
/**
|
|
35
|
+
* Clear all resource states
|
|
36
|
+
*/
|
|
37
|
+
clearResourceStates(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Start a new deployment
|
|
40
|
+
*/
|
|
41
|
+
startDeployment(stackName: string, nodes: SerializedNode[], changeStats?: {
|
|
42
|
+
creates: number;
|
|
43
|
+
updates: number;
|
|
44
|
+
deletes: number;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Update a node's outputs in persisted state (for crash recovery)
|
|
48
|
+
* This is the real checkpoint - nodes with outputs are considered deployed
|
|
49
|
+
*/
|
|
50
|
+
updateNodeOutputs(stackName: string, nodeId: string, outputs: Record<string, any>): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Mark a node as currently being applied (for crash recovery)
|
|
53
|
+
*/
|
|
54
|
+
markApplying(stackName: string, nodeId: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Clear the applying marker after successful apply
|
|
57
|
+
*/
|
|
58
|
+
clearApplying(stackName: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Record a resource being applied
|
|
61
|
+
*/
|
|
62
|
+
recordResourceApplied(stackName: string, nodeId: string, outputs: Record<string, any>): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Record a resource being destroyed
|
|
65
|
+
*/
|
|
66
|
+
recordResourceDestroyed(stackName: string, nodeId: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Complete a successful deployment
|
|
69
|
+
*/
|
|
70
|
+
completeDeployment(stackName: string, nodes: SerializedNode[]): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Record a failed deployment
|
|
73
|
+
*/
|
|
74
|
+
failDeployment(stackName: string, error: Error): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Check if a deployment can be resumed
|
|
77
|
+
*/
|
|
78
|
+
canResume(stackName: string): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Get the node that was being applied when crash occurred
|
|
81
|
+
*/
|
|
82
|
+
getInterruptedNodeId(stackName: string): Promise<string | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Get previous deployment state
|
|
85
|
+
*/
|
|
86
|
+
getPreviousState(stackName: string): Promise<DeploymentState | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Acquire deployment lock (if backend supports it)
|
|
89
|
+
*/
|
|
90
|
+
acquireLock(stackName: string, holder: string, ttlSeconds?: number): Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Release deployment lock (if backend supports it)
|
|
93
|
+
*/
|
|
94
|
+
releaseLock(stackName: string): Promise<void>;
|
|
95
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateMachine - Resource lifecycle tracking and deployment orchestration
|
|
3
|
+
*
|
|
4
|
+
* Tracks:
|
|
5
|
+
* - Individual resource states (pending → applying → deployed)
|
|
6
|
+
* - Deployment lifecycle (start → checkpoint → complete/fail)
|
|
7
|
+
* - Resume capability from crash
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* StateMachine manages deployment lifecycle and resource states
|
|
11
|
+
*/
|
|
12
|
+
export class StateMachine {
|
|
13
|
+
backend;
|
|
14
|
+
resourceStates = new Map();
|
|
15
|
+
options;
|
|
16
|
+
constructor(backend, options = {}) {
|
|
17
|
+
this.backend = backend;
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the current state of a resource
|
|
22
|
+
*/
|
|
23
|
+
getResourceState(nodeId) {
|
|
24
|
+
return this.resourceStates.get(nodeId) ?? 'pending';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set the state of a resource
|
|
28
|
+
*/
|
|
29
|
+
setResourceState(nodeId, state) {
|
|
30
|
+
this.resourceStates.set(nodeId, state);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Restore resource states from persisted nodes
|
|
34
|
+
*/
|
|
35
|
+
restoreResourceStates(nodes) {
|
|
36
|
+
this.resourceStates.clear();
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
// If node has outputs, it was previously deployed
|
|
39
|
+
const state = node.state ?? (node.outputs ? 'deployed' : 'pending');
|
|
40
|
+
this.resourceStates.set(node.id, state);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Clear all resource states
|
|
45
|
+
*/
|
|
46
|
+
clearResourceStates() {
|
|
47
|
+
this.resourceStates.clear();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Start a new deployment
|
|
51
|
+
*/
|
|
52
|
+
async startDeployment(stackName, nodes, changeStats) {
|
|
53
|
+
const state = {
|
|
54
|
+
nodes,
|
|
55
|
+
status: 'applying',
|
|
56
|
+
stackName,
|
|
57
|
+
lastDeployedAt: Date.now(),
|
|
58
|
+
user: this.options.user,
|
|
59
|
+
};
|
|
60
|
+
await this.backend.saveState(stackName, state);
|
|
61
|
+
if (this.options.enableAuditLog && this.backend.appendAuditLog) {
|
|
62
|
+
await this.backend.appendAuditLog(stackName, {
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
action: 'deploy_start',
|
|
65
|
+
user: this.options.user,
|
|
66
|
+
details: changeStats,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Update a node's outputs in persisted state (for crash recovery)
|
|
72
|
+
* This is the real checkpoint - nodes with outputs are considered deployed
|
|
73
|
+
*/
|
|
74
|
+
async updateNodeOutputs(stackName, nodeId, outputs) {
|
|
75
|
+
const state = await this.backend.getState(stackName);
|
|
76
|
+
if (state) {
|
|
77
|
+
const node = state.nodes.find((n) => n.id === nodeId);
|
|
78
|
+
if (node) {
|
|
79
|
+
node.outputs = outputs;
|
|
80
|
+
await this.backend.saveState(stackName, state);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Mark a node as currently being applied (for crash recovery)
|
|
86
|
+
*/
|
|
87
|
+
async markApplying(stackName, nodeId) {
|
|
88
|
+
const state = await this.backend.getState(stackName);
|
|
89
|
+
if (state) {
|
|
90
|
+
state.applyingNodeId = nodeId;
|
|
91
|
+
await this.backend.saveState(stackName, state);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Clear the applying marker after successful apply
|
|
96
|
+
*/
|
|
97
|
+
async clearApplying(stackName) {
|
|
98
|
+
const state = await this.backend.getState(stackName);
|
|
99
|
+
if (state) {
|
|
100
|
+
state.applyingNodeId = undefined;
|
|
101
|
+
await this.backend.saveState(stackName, state);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Record a resource being applied
|
|
106
|
+
*/
|
|
107
|
+
async recordResourceApplied(stackName, nodeId, outputs) {
|
|
108
|
+
this.setResourceState(nodeId, 'deployed');
|
|
109
|
+
if (this.options.enableAuditLog && this.backend.appendAuditLog) {
|
|
110
|
+
await this.backend.appendAuditLog(stackName, {
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
action: 'resource_applied',
|
|
113
|
+
nodeId,
|
|
114
|
+
user: this.options.user,
|
|
115
|
+
details: { outputKeys: Object.keys(outputs) },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Record a resource being destroyed
|
|
121
|
+
*/
|
|
122
|
+
async recordResourceDestroyed(stackName, nodeId) {
|
|
123
|
+
this.resourceStates.delete(nodeId);
|
|
124
|
+
if (this.options.enableAuditLog && this.backend.appendAuditLog) {
|
|
125
|
+
await this.backend.appendAuditLog(stackName, {
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
action: 'resource_destroyed',
|
|
128
|
+
nodeId,
|
|
129
|
+
user: this.options.user,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Complete a successful deployment
|
|
135
|
+
*/
|
|
136
|
+
async completeDeployment(stackName, nodes) {
|
|
137
|
+
const state = {
|
|
138
|
+
nodes,
|
|
139
|
+
status: 'deployed',
|
|
140
|
+
stackName,
|
|
141
|
+
lastDeployedAt: Date.now(),
|
|
142
|
+
user: this.options.user,
|
|
143
|
+
};
|
|
144
|
+
await this.backend.saveState(stackName, state);
|
|
145
|
+
if (this.options.enableAuditLog && this.backend.appendAuditLog) {
|
|
146
|
+
await this.backend.appendAuditLog(stackName, {
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
action: 'deploy_complete',
|
|
149
|
+
user: this.options.user,
|
|
150
|
+
details: { nodeCount: nodes.length },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Record a failed deployment
|
|
156
|
+
*/
|
|
157
|
+
async failDeployment(stackName, error) {
|
|
158
|
+
const state = await this.backend.getState(stackName);
|
|
159
|
+
if (state) {
|
|
160
|
+
state.status = 'failed';
|
|
161
|
+
await this.backend.saveState(stackName, state);
|
|
162
|
+
}
|
|
163
|
+
if (this.options.enableAuditLog && this.backend.appendAuditLog) {
|
|
164
|
+
await this.backend.appendAuditLog(stackName, {
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
action: 'deploy_failed',
|
|
167
|
+
user: this.options.user,
|
|
168
|
+
details: { error: error.message },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check if a deployment can be resumed
|
|
174
|
+
*/
|
|
175
|
+
async canResume(stackName) {
|
|
176
|
+
const state = await this.backend.getState(stackName);
|
|
177
|
+
return state?.status === 'applying';
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get the node that was being applied when crash occurred
|
|
181
|
+
*/
|
|
182
|
+
async getInterruptedNodeId(stackName) {
|
|
183
|
+
const state = await this.backend.getState(stackName);
|
|
184
|
+
return state?.applyingNodeId ?? null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get previous deployment state
|
|
188
|
+
*/
|
|
189
|
+
async getPreviousState(stackName) {
|
|
190
|
+
return this.backend.getState(stackName);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Acquire deployment lock (if backend supports it)
|
|
194
|
+
*/
|
|
195
|
+
async acquireLock(stackName, holder, ttlSeconds = 300) {
|
|
196
|
+
if (this.backend.acquireLock) {
|
|
197
|
+
return this.backend.acquireLock(stackName, holder, ttlSeconds);
|
|
198
|
+
}
|
|
199
|
+
return true; // No locking support, allow
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Release deployment lock (if backend supports it)
|
|
203
|
+
*/
|
|
204
|
+
async releaseLock(stackName) {
|
|
205
|
+
if (this.backend.releaseLock) {
|
|
206
|
+
await this.backend.releaseLock(stackName);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types
|
|
3
|
+
*/
|
|
4
|
+
export type { JSXElement } from './jsx/jsx-runtime';
|
|
5
|
+
export type { Context } from './primitives/context';
|
|
6
|
+
export type { InstanceNode, OutputAccessors } from './primitives/instance';
|
|
7
|
+
export type { SetStoreFunction } from './primitives/store';
|
|
8
|
+
export type { AuditLogEntry, Backend, DeploymentState, DeploymentStatus, ResourceState, SerializedNode, } from './provider/backend';
|
|
9
|
+
export type { Provider } from './provider/interface';
|
|
10
|
+
export type { Accessor, Computation, Setter, Signal } from './reactive/signal';
|
|
11
|
+
export type { Fiber } from './runtime/fiber';
|
|
12
|
+
export type { ChangeSet, DependencyGraph } from './runtime/reconcile';
|
|
13
|
+
export type { StateMachineOptions } from './runtime/state-machine';
|
package/dist/types.js
ADDED