@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.
- package/README.md +72 -21
- 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 +11 -27
- 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,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInstance - bind to a provider and get reactive outputs
|
|
3
|
+
*/
|
|
4
|
+
import { createSignal } from '../reactive/signal';
|
|
5
|
+
import { batch } from '../reactive/tracking';
|
|
6
|
+
import { getCurrentFiber, getCurrentResourcePath, pushResourcePath } from '../runtime/render';
|
|
7
|
+
// Registry of all instance nodes by ID
|
|
8
|
+
const nodeRegistry = new Map();
|
|
9
|
+
// Track which fiber owns each nodeId (to detect duplicate siblings)
|
|
10
|
+
const nodeOwnership = new Map();
|
|
11
|
+
// Output hydration map - populated before render to restore outputs from previous run
|
|
12
|
+
const outputHydrationMap = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Prepare output hydration from serialized nodes
|
|
15
|
+
* Called by runtime BEFORE rendering to restore outputs
|
|
16
|
+
*/
|
|
17
|
+
export function prepareOutputHydration(serializedNodes) {
|
|
18
|
+
outputHydrationMap.clear();
|
|
19
|
+
for (const node of serializedNodes) {
|
|
20
|
+
if (node.outputs && Object.keys(node.outputs).length > 0) {
|
|
21
|
+
outputHydrationMap.set(node.id, node.outputs);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Clear output hydration map
|
|
27
|
+
*/
|
|
28
|
+
export function clearOutputHydration() {
|
|
29
|
+
outputHydrationMap.clear();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a placeholder proxy that returns undefined for all outputs
|
|
33
|
+
* Used when props have undefined dependencies - node won't be created yet
|
|
34
|
+
*/
|
|
35
|
+
function createPlaceholderProxy() {
|
|
36
|
+
return new Proxy({}, {
|
|
37
|
+
get(_target, _key) {
|
|
38
|
+
return () => undefined; // All outputs return undefined
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create an instance bound to a provider
|
|
44
|
+
*/
|
|
45
|
+
export function useInstance(construct, props) {
|
|
46
|
+
const fiber = getCurrentFiber();
|
|
47
|
+
if (!fiber) {
|
|
48
|
+
throw new Error('useInstance must be called during render');
|
|
49
|
+
}
|
|
50
|
+
// Enforce one instance per component - forces proper JSX composition
|
|
51
|
+
// Each component = one resource, compose via children
|
|
52
|
+
if (fiber.instanceNodes.length > 0 || fiber.hasPlaceholderInstance) {
|
|
53
|
+
throw new Error('useInstance can only be called once per component. ' +
|
|
54
|
+
'Use child components for additional resources:\n\n' +
|
|
55
|
+
' function MyStack() {\n' +
|
|
56
|
+
' const db = useInstance(Database, { name: "main" });\n' +
|
|
57
|
+
' return <Cache dbUrl={db.url()} />;\n' +
|
|
58
|
+
' }\n\n' +
|
|
59
|
+
' function Cache({ dbUrl }) {\n' +
|
|
60
|
+
' useInstance(CacheService, { db: dbUrl });\n' +
|
|
61
|
+
' return null;\n' +
|
|
62
|
+
' }');
|
|
63
|
+
}
|
|
64
|
+
// Generate deterministic ID using resource path (not fiber path)
|
|
65
|
+
// This makes wrapper components transparent - they don't affect resource IDs
|
|
66
|
+
const currentResourcePath = getCurrentResourcePath();
|
|
67
|
+
const name = getNodeName(construct, fiber.key);
|
|
68
|
+
const fullPath = [...currentResourcePath, name];
|
|
69
|
+
const nodeId = fullPath.join('.');
|
|
70
|
+
// Check for undefined dependencies BEFORE creating/updating node
|
|
71
|
+
// If any prop is undefined, return placeholder - don't create node in registry
|
|
72
|
+
const hasUndefinedDeps = Object.values(props).some((v) => v === undefined);
|
|
73
|
+
if (hasUndefinedDeps) {
|
|
74
|
+
// STILL push to resource path so children have correct paths
|
|
75
|
+
pushResourcePath(name);
|
|
76
|
+
// Mark fiber as having a placeholder instance (for proper path pop in render.ts)
|
|
77
|
+
fiber.hasPlaceholderInstance = true;
|
|
78
|
+
return createPlaceholderProxy();
|
|
79
|
+
}
|
|
80
|
+
const constructType = construct.name || 'Unknown';
|
|
81
|
+
// Check for duplicate siblings (requires keys)
|
|
82
|
+
// Allow same fiber to re-register (reactive re-render), but not different fibers
|
|
83
|
+
const existingOwner = nodeOwnership.get(nodeId);
|
|
84
|
+
if (existingOwner && existingOwner !== fiber) {
|
|
85
|
+
throw new Error(`Multiple instances of ${constructType} at the same level require unique keys.\n` +
|
|
86
|
+
`Add a key prop to differentiate them:\n\n` +
|
|
87
|
+
` {items.map((item) => (\n` +
|
|
88
|
+
` <MyResource key={item.id} ... />\n` +
|
|
89
|
+
` ))}`);
|
|
90
|
+
}
|
|
91
|
+
nodeOwnership.set(nodeId, fiber);
|
|
92
|
+
// Create or get existing node
|
|
93
|
+
let node = nodeRegistry.get(nodeId);
|
|
94
|
+
if (!node) {
|
|
95
|
+
node = {
|
|
96
|
+
id: nodeId,
|
|
97
|
+
path: fullPath,
|
|
98
|
+
construct,
|
|
99
|
+
constructType,
|
|
100
|
+
props,
|
|
101
|
+
outputSignals: new Map(),
|
|
102
|
+
children: [],
|
|
103
|
+
setOutputs(outputs) {
|
|
104
|
+
// Clear ownership before batch triggers re-renders
|
|
105
|
+
// When signals update, components re-execute and create new fibers
|
|
106
|
+
// that need to claim the same nodeIds
|
|
107
|
+
nodeOwnership.clear();
|
|
108
|
+
batch(() => {
|
|
109
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
110
|
+
if (!this.outputSignals.has(key)) {
|
|
111
|
+
this.outputSignals.set(key, createSignal(value));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const [, write] = this.outputSignals.get(key);
|
|
115
|
+
write(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
nodeRegistry.set(nodeId, node);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Update props
|
|
125
|
+
node.props = props;
|
|
126
|
+
}
|
|
127
|
+
// Push to resource path so children see this component in their path
|
|
128
|
+
pushResourcePath(name);
|
|
129
|
+
// Hydrate outputs from previous run (if available)
|
|
130
|
+
const hydratedOutputs = outputHydrationMap.get(nodeId);
|
|
131
|
+
if (hydratedOutputs) {
|
|
132
|
+
for (const [key, value] of Object.entries(hydratedOutputs)) {
|
|
133
|
+
if (!node.outputSignals.has(key)) {
|
|
134
|
+
node.outputSignals.set(key, createSignal(value));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const [, write] = node.outputSignals.get(key);
|
|
138
|
+
write(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Attach to fiber
|
|
143
|
+
fiber.instanceNodes.push(node);
|
|
144
|
+
// Return proxy where each property access returns a wrapper function
|
|
145
|
+
// that auto-unwraps accessors (SolidJS-style)
|
|
146
|
+
return new Proxy({}, {
|
|
147
|
+
get(_, key) {
|
|
148
|
+
// Lazily create signal for this output
|
|
149
|
+
if (!node.outputSignals.has(key)) {
|
|
150
|
+
node.outputSignals.set(key, createSignal());
|
|
151
|
+
}
|
|
152
|
+
// biome-ignore lint/style/noNonNullAssertion: we just ensured the key exists above
|
|
153
|
+
const [read] = node.outputSignals.get(key);
|
|
154
|
+
// Return wrapper that auto-unwraps accessors
|
|
155
|
+
return () => {
|
|
156
|
+
const value = read(); // Read from signal (tracks it)
|
|
157
|
+
// If provider returned an accessor, call it (tracks provider's signal)
|
|
158
|
+
if (typeof value === 'function') {
|
|
159
|
+
return value();
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Generate a node name from construct and fiber key
|
|
168
|
+
*/
|
|
169
|
+
function getNodeName(construct, key) {
|
|
170
|
+
const baseName = construct.name || 'Instance';
|
|
171
|
+
const kebab = toKebabCase(baseName);
|
|
172
|
+
if (key !== undefined) {
|
|
173
|
+
return `${kebab}-${key}`;
|
|
174
|
+
}
|
|
175
|
+
return kebab;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Convert PascalCase to kebab-case
|
|
179
|
+
*/
|
|
180
|
+
function toKebabCase(str) {
|
|
181
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Fill instance outputs (called by runtime after provider returns)
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
export function fillInstanceOutputs(nodeId, outputs) {
|
|
188
|
+
const node = nodeRegistry.get(nodeId);
|
|
189
|
+
if (!node)
|
|
190
|
+
return;
|
|
191
|
+
// Clear ownership before batch triggers re-renders
|
|
192
|
+
// When signals update, components re-execute and create new fibers
|
|
193
|
+
// that need to claim the same nodeIds
|
|
194
|
+
nodeOwnership.clear();
|
|
195
|
+
batch(() => {
|
|
196
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
197
|
+
if (node.outputSignals.has(key)) {
|
|
198
|
+
const [, write] = node.outputSignals.get(key);
|
|
199
|
+
write(value); // Write value (plain or accessor) to signal
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
node.outputSignals.set(key, createSignal(value));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get a node by ID
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
211
|
+
export function getNodeById(nodeId) {
|
|
212
|
+
return nodeRegistry.get(nodeId);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get all registered nodes
|
|
216
|
+
* @internal
|
|
217
|
+
*/
|
|
218
|
+
export function getAllNodes() {
|
|
219
|
+
return Array.from(nodeRegistry.values());
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clear node registry (for testing)
|
|
223
|
+
* @internal
|
|
224
|
+
*/
|
|
225
|
+
export function clearNodeRegistry() {
|
|
226
|
+
nodeRegistry.clear();
|
|
227
|
+
nodeOwnership.clear();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Clear node ownership (call at start of each render pass)
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
export function clearNodeOwnership() {
|
|
234
|
+
nodeOwnership.clear();
|
|
235
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store - persistent state (non-reactive)
|
|
3
|
+
*/
|
|
4
|
+
export type SetStoreFunction<T> = {
|
|
5
|
+
<K extends keyof T>(key: K, value: T[K] | ((prev: T[K]) => T[K])): void;
|
|
6
|
+
<K1 extends keyof T, K2 extends keyof T[K1]>(k1: K1, k2: K2, value: T[K1][K2] | ((prev: T[K1][K2]) => T[K1][K2])): void;
|
|
7
|
+
(...args: any[]): void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Create a persistent store (non-reactive)
|
|
11
|
+
*/
|
|
12
|
+
export declare function createStore<T extends object>(initial: T): [T, SetStoreFunction<T>];
|
|
13
|
+
/**
|
|
14
|
+
* Prepare hydration from previous nodes
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function prepareHydration(previousNodes: any[]): void;
|
|
18
|
+
/**
|
|
19
|
+
* Clear hydration map (for testing)
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearHydration(): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store - persistent state (non-reactive)
|
|
3
|
+
*/
|
|
4
|
+
import { getCurrentFiber } from '../runtime/render';
|
|
5
|
+
// Hydration map for restoring state across cycles
|
|
6
|
+
// biome-ignore lint/suspicious/noExplicitAny: stores hold user-defined state of any type
|
|
7
|
+
const hydrationMap = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Create a persistent store (non-reactive)
|
|
10
|
+
*/
|
|
11
|
+
export function createStore(initial) {
|
|
12
|
+
const fiber = getCurrentFiber();
|
|
13
|
+
// Try to hydrate from previous cycle
|
|
14
|
+
const hydrated = fiber ? hydrateStore(fiber.path) : undefined;
|
|
15
|
+
const state = hydrated ?? { ...initial };
|
|
16
|
+
// Mark for persistence
|
|
17
|
+
if (fiber) {
|
|
18
|
+
fiber.store = state;
|
|
19
|
+
}
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: rest args for deep path updates
|
|
21
|
+
function setStore(...args) {
|
|
22
|
+
updatePath(state, args);
|
|
23
|
+
}
|
|
24
|
+
return [state, setStore];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Update a nested path in an object
|
|
28
|
+
*/
|
|
29
|
+
// biome-ignore lint/suspicious/noExplicitAny: operates on arbitrary nested objects
|
|
30
|
+
function updatePath(obj, args) {
|
|
31
|
+
if (args.length === 2) {
|
|
32
|
+
const [key, value] = args;
|
|
33
|
+
obj[key] = typeof value === 'function' ? value(obj[key]) : value;
|
|
34
|
+
}
|
|
35
|
+
else if (args.length > 2) {
|
|
36
|
+
const key = args[0];
|
|
37
|
+
if (obj[key] === undefined) {
|
|
38
|
+
obj[key] = {};
|
|
39
|
+
}
|
|
40
|
+
updatePath(obj[key], args.slice(1));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Prepare hydration from previous nodes
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts serialized nodes with arbitrary structure
|
|
48
|
+
export function prepareHydration(previousNodes) {
|
|
49
|
+
hydrationMap.clear();
|
|
50
|
+
for (const node of flattenNodes(previousNodes)) {
|
|
51
|
+
if (node.store) {
|
|
52
|
+
// Key by component path (parent of node)
|
|
53
|
+
const componentPath = node.path.slice(0, -1).join('.');
|
|
54
|
+
hydrationMap.set(componentPath, node.store);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Hydrate store from previous cycle
|
|
60
|
+
* Returns a deep clone to ensure previous and current stores are independent
|
|
61
|
+
*/
|
|
62
|
+
function hydrateStore(fiberPath) {
|
|
63
|
+
if (!fiberPath)
|
|
64
|
+
return undefined;
|
|
65
|
+
const key = fiberPath.join('.');
|
|
66
|
+
const stored = hydrationMap.get(key);
|
|
67
|
+
// Deep clone to ensure independence between runs
|
|
68
|
+
return stored ? JSON.parse(JSON.stringify(stored)) : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Flatten nested nodes
|
|
72
|
+
*/
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: operates on serialized nodes with arbitrary structure
|
|
74
|
+
function flattenNodes(nodes) {
|
|
75
|
+
// biome-ignore lint/suspicious/noExplicitAny: accumulates serialized nodes
|
|
76
|
+
const result = [];
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: serialized node structure
|
|
78
|
+
function walk(node) {
|
|
79
|
+
result.push(node);
|
|
80
|
+
if (node.children) {
|
|
81
|
+
for (const child of node.children) {
|
|
82
|
+
walk(child);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const node of nodes) {
|
|
87
|
+
walk(node);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clear hydration map (for testing)
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
export function clearHydration() {
|
|
96
|
+
hydrationMap.clear();
|
|
97
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend interface - abstracts state persistence
|
|
3
|
+
*
|
|
4
|
+
* The Backend enables:
|
|
5
|
+
* - Resume from crash (nodes with outputs are treated as deployed)
|
|
6
|
+
* - Incremental deploys (only changed resources)
|
|
7
|
+
* - State hydration (createStore values restored)
|
|
8
|
+
* - Drift detection (compare expected vs actual)
|
|
9
|
+
*/
|
|
10
|
+
import type { InstanceNode } from '../primitives/instance';
|
|
11
|
+
/**
|
|
12
|
+
* Resource deployment state
|
|
13
|
+
*/
|
|
14
|
+
export type ResourceState = 'pending' | 'applying' | 'deployed' | 'failed';
|
|
15
|
+
/**
|
|
16
|
+
* Deployment lifecycle status
|
|
17
|
+
*/
|
|
18
|
+
export type DeploymentStatus = 'pending' | 'applying' | 'deployed' | 'failed';
|
|
19
|
+
/**
|
|
20
|
+
* Change set with deployment ordering
|
|
21
|
+
*/
|
|
22
|
+
export interface ChangeSet {
|
|
23
|
+
creates: InstanceNode[];
|
|
24
|
+
updates: InstanceNode[];
|
|
25
|
+
deletes: InstanceNode[];
|
|
26
|
+
deploymentOrder: string[];
|
|
27
|
+
parallelBatches: string[][];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Serializable node state for persistence
|
|
31
|
+
*/
|
|
32
|
+
export interface SerializedNode {
|
|
33
|
+
id: string;
|
|
34
|
+
path: string[];
|
|
35
|
+
constructType: string;
|
|
36
|
+
props: Record<string, any>;
|
|
37
|
+
outputs?: Record<string, any>;
|
|
38
|
+
state?: ResourceState;
|
|
39
|
+
store?: any;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Deployment state - persisted by backend
|
|
43
|
+
*/
|
|
44
|
+
export interface DeploymentState {
|
|
45
|
+
nodes: SerializedNode[];
|
|
46
|
+
status: DeploymentStatus;
|
|
47
|
+
applyingNodeId?: string;
|
|
48
|
+
stackName: string;
|
|
49
|
+
lastDeployedAt: number;
|
|
50
|
+
user?: string;
|
|
51
|
+
storeValues?: Record<string, any>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Audit log entry for tracking deployment history
|
|
55
|
+
*/
|
|
56
|
+
export interface AuditLogEntry {
|
|
57
|
+
timestamp: number;
|
|
58
|
+
action: 'deploy_start' | 'deploy_complete' | 'deploy_failed' | 'resource_applied' | 'resource_destroyed';
|
|
59
|
+
nodeId?: string;
|
|
60
|
+
details?: Record<string, any>;
|
|
61
|
+
user?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Backend interface - implement this to persist deployment state
|
|
65
|
+
*
|
|
66
|
+
* Example implementations:
|
|
67
|
+
* - InMemoryBackend: For testing
|
|
68
|
+
* - FileBackend: Local JSON file
|
|
69
|
+
* - DynamoDBBackend: AWS DynamoDB
|
|
70
|
+
* - S3Backend: AWS S3
|
|
71
|
+
* - PostgresBackend: Database
|
|
72
|
+
*/
|
|
73
|
+
export interface Backend {
|
|
74
|
+
/**
|
|
75
|
+
* Get current deployment state for a stack
|
|
76
|
+
*/
|
|
77
|
+
getState(stackName: string): Promise<DeploymentState | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Save deployment state
|
|
80
|
+
*/
|
|
81
|
+
saveState(stackName: string, state: DeploymentState): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Optional: Acquire lock for concurrent deploy protection
|
|
84
|
+
* @param stackName - Stack to lock
|
|
85
|
+
* @param holder - Identity of lock holder (e.g., deployment ID)
|
|
86
|
+
* @param ttlSeconds - Lock TTL in seconds
|
|
87
|
+
* @returns true if lock acquired, false if already locked
|
|
88
|
+
*/
|
|
89
|
+
acquireLock?(stackName: string, holder: string, ttlSeconds: number): Promise<boolean>;
|
|
90
|
+
/**
|
|
91
|
+
* Optional: Release deployment lock
|
|
92
|
+
*/
|
|
93
|
+
releaseLock?(stackName: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Optional: Append to audit trail
|
|
96
|
+
*/
|
|
97
|
+
appendAuditLog?(stackName: string, entry: AuditLogEntry): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Optional: Get audit log entries
|
|
100
|
+
*/
|
|
101
|
+
getAuditLog?(stackName: string, limit?: number): Promise<AuditLogEntry[]>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Serialize an InstanceNode for persistence
|
|
105
|
+
*/
|
|
106
|
+
export declare function serializeNode(node: InstanceNode): SerializedNode;
|
|
107
|
+
/**
|
|
108
|
+
* Serialize multiple nodes
|
|
109
|
+
*/
|
|
110
|
+
export declare function serializeNodes(nodes: InstanceNode[]): SerializedNode[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend interface - abstracts state persistence
|
|
3
|
+
*
|
|
4
|
+
* The Backend enables:
|
|
5
|
+
* - Resume from crash (nodes with outputs are treated as deployed)
|
|
6
|
+
* - Incremental deploys (only changed resources)
|
|
7
|
+
* - State hydration (createStore values restored)
|
|
8
|
+
* - Drift detection (compare expected vs actual)
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Serialize an InstanceNode for persistence
|
|
12
|
+
*/
|
|
13
|
+
export function serializeNode(node) {
|
|
14
|
+
// Extract current output values from signals
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: outputs are provider-returned with arbitrary types
|
|
16
|
+
const outputs = {};
|
|
17
|
+
for (const [key, [read]] of node.outputSignals) {
|
|
18
|
+
const value = read();
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
outputs[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
id: node.id,
|
|
25
|
+
path: node.path,
|
|
26
|
+
constructType: node.constructType,
|
|
27
|
+
props: node.props,
|
|
28
|
+
outputs: Object.keys(outputs).length > 0 ? outputs : undefined,
|
|
29
|
+
store: node.store,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Serialize multiple nodes
|
|
34
|
+
*/
|
|
35
|
+
export function serializeNodes(nodes) {
|
|
36
|
+
return nodes.map(serializeNode);
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interface - abstracts the execution environment
|
|
3
|
+
*/
|
|
4
|
+
import type { InstanceNode } from '../primitives/instance';
|
|
5
|
+
/**
|
|
6
|
+
* Event emitted when provider detects output changes
|
|
7
|
+
*/
|
|
8
|
+
export interface OutputChangeEvent {
|
|
9
|
+
resourceName: string;
|
|
10
|
+
outputs: Record<string, any>;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provider interface - implement this to connect to any backend
|
|
15
|
+
*/
|
|
16
|
+
export interface Provider {
|
|
17
|
+
/** Initialize provider (async setup) */
|
|
18
|
+
initialize?(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Materialize nodes into cloud resources.
|
|
21
|
+
*
|
|
22
|
+
* Providers should call node.setOutputs() when outputs are available.
|
|
23
|
+
* This triggers reactive updates - dependent components re-render automatically.
|
|
24
|
+
*
|
|
25
|
+
* For sync providers: call node.setOutputs() immediately
|
|
26
|
+
* For async providers: call node.setOutputs() in the async callback
|
|
27
|
+
*
|
|
28
|
+
* Returns a Promise that resolves when all resources are materialized.
|
|
29
|
+
*/
|
|
30
|
+
materialize(nodes: InstanceNode[]): Promise<void>;
|
|
31
|
+
/** Destroy a node */
|
|
32
|
+
destroy(node: InstanceNode): Promise<void>;
|
|
33
|
+
/** Lifecycle hooks */
|
|
34
|
+
preDeploy?(nodes: InstanceNode[]): Promise<void>;
|
|
35
|
+
postDeploy?(nodes: InstanceNode[], outputs: Record<string, any>): Promise<void>;
|
|
36
|
+
onError?(error: Error, nodes: InstanceNode[]): Promise<void>;
|
|
37
|
+
/** Event system (required for continuous runtime) */
|
|
38
|
+
on(event: 'outputsChanged', handler: (change: OutputChangeEvent) => void): void;
|
|
39
|
+
off(event: 'outputsChanged', handler: (change: OutputChangeEvent) => void): void;
|
|
40
|
+
stop(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a mock provider for testing
|
|
44
|
+
*/
|
|
45
|
+
export declare function createMockProvider(handlers?: Partial<{
|
|
46
|
+
materialize: (nodes: InstanceNode[]) => Promise<void> | void;
|
|
47
|
+
destroy: (node: InstanceNode) => Promise<void> | void;
|
|
48
|
+
}>): Provider;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interface - abstracts the execution environment
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create a mock provider for testing
|
|
6
|
+
*/
|
|
7
|
+
export function createMockProvider(handlers = {}) {
|
|
8
|
+
const eventHandlers = new Map();
|
|
9
|
+
return {
|
|
10
|
+
async materialize(nodes) {
|
|
11
|
+
if (handlers.materialize) {
|
|
12
|
+
await handlers.materialize(nodes);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// Default: set empty outputs via reactive API
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
node.setOutputs({});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async destroy(node) {
|
|
22
|
+
if (handlers.destroy) {
|
|
23
|
+
await handlers.destroy(node);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
on(event, handler) {
|
|
27
|
+
if (!eventHandlers.has(event)) {
|
|
28
|
+
eventHandlers.set(event, new Set());
|
|
29
|
+
}
|
|
30
|
+
eventHandlers.get(event)?.add(handler);
|
|
31
|
+
},
|
|
32
|
+
off(event, handler) {
|
|
33
|
+
eventHandlers.get(event)?.delete(handler);
|
|
34
|
+
},
|
|
35
|
+
stop() {
|
|
36
|
+
eventHandlers.clear();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createEffect - run side effects when dependencies change
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create a reactive effect that runs when dependencies change
|
|
6
|
+
*/
|
|
7
|
+
export declare function createEffect(fn: () => undefined | (() => void)): void;
|
|
8
|
+
/**
|
|
9
|
+
* Register a cleanup function for the current computation
|
|
10
|
+
*/
|
|
11
|
+
export declare function onCleanup(fn: () => void): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createEffect - run side effects when dependencies change
|
|
3
|
+
*/
|
|
4
|
+
import { getListener, runComputation } from './tracking';
|
|
5
|
+
/**
|
|
6
|
+
* Create a reactive effect that runs when dependencies change
|
|
7
|
+
*/
|
|
8
|
+
export function createEffect(fn) {
|
|
9
|
+
const computation = {
|
|
10
|
+
fn: () => {
|
|
11
|
+
const cleanup = fn();
|
|
12
|
+
if (typeof cleanup === 'function') {
|
|
13
|
+
if (!computation.cleanups) {
|
|
14
|
+
computation.cleanups = [cleanup];
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
computation.cleanups.push(cleanup);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
sources: null,
|
|
22
|
+
sourceSlots: null,
|
|
23
|
+
state: 1, // STALE - needs initial run
|
|
24
|
+
cleanups: null,
|
|
25
|
+
};
|
|
26
|
+
// Run immediately
|
|
27
|
+
runComputation(computation);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Register a cleanup function for the current computation
|
|
31
|
+
*/
|
|
32
|
+
export function onCleanup(fn) {
|
|
33
|
+
const listener = getListener();
|
|
34
|
+
if (listener) {
|
|
35
|
+
if (!listener.cleanups) {
|
|
36
|
+
listener.cleanups = [fn];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
listener.cleanups.push(fn);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createEffect, onCleanup } from './effect';
|
|
2
|
+
export { type Accessor, type Computation, createSignal, type Setter, type Signal } from './signal';
|
|
3
|
+
export { batch, cleanComputation, flushSync, getListener, runComputation, scheduleComputation, setListener, untrack, } from './tracking';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal - core reactive primitive (internal)
|
|
3
|
+
* Inspired by SolidJS createSignal
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Signal state - holds value and tracks observers
|
|
7
|
+
*/
|
|
8
|
+
export interface Signal<T> {
|
|
9
|
+
value: T | undefined;
|
|
10
|
+
observers: Computation<any>[] | null;
|
|
11
|
+
observerSlots: number[] | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Computation - tracks dependencies and re-runs when they change
|
|
15
|
+
*/
|
|
16
|
+
export interface Computation<T> {
|
|
17
|
+
fn: () => T;
|
|
18
|
+
sources: Signal<any>[] | null;
|
|
19
|
+
sourceSlots: number[] | null;
|
|
20
|
+
state: 0 | 1 | 2;
|
|
21
|
+
cleanups: (() => void)[] | null;
|
|
22
|
+
}
|
|
23
|
+
export type Accessor<T> = () => T | undefined;
|
|
24
|
+
export type Setter<T> = (value: T) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Create a reactive signal (internal - not exported from package)
|
|
27
|
+
*/
|
|
28
|
+
export declare function createSignal<T>(initial?: T): [Accessor<T>, Setter<T>];
|
|
29
|
+
/**
|
|
30
|
+
* Get raw signal value without tracking
|
|
31
|
+
*/
|
|
32
|
+
export declare function peekSignal<T>(signal: Signal<T>): T | undefined;
|