@constela/runtime 0.11.1 → 0.12.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 +102 -1
- package/dist/index.d.ts +156 -1
- package/dist/index.js +687 -86
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -40,6 +40,81 @@ Becomes an interactive app with:
|
|
|
40
40
|
|
|
41
41
|
## Features
|
|
42
42
|
|
|
43
|
+
### Fine-grained State Updates (setPath)
|
|
44
|
+
|
|
45
|
+
Update nested values without replacing entire arrays:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"do": "setPath",
|
|
50
|
+
"target": "posts",
|
|
51
|
+
"path": [5, "liked"],
|
|
52
|
+
"value": { "expr": "lit", "value": true }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Dynamic path with variables:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"do": "setPath",
|
|
61
|
+
"target": "posts",
|
|
62
|
+
"path": { "expr": "var", "name": "payload", "path": "index" },
|
|
63
|
+
"field": "liked",
|
|
64
|
+
"value": { "expr": "lit", "value": true }
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Key-based List Diffing
|
|
69
|
+
|
|
70
|
+
Efficient list updates - only changed items re-render:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"kind": "each",
|
|
75
|
+
"items": { "expr": "state", "name": "posts" },
|
|
76
|
+
"as": "post",
|
|
77
|
+
"key": { "expr": "var", "name": "post", "path": "id" },
|
|
78
|
+
"body": { ... }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Benefits:
|
|
83
|
+
- Add/remove items: Only affected DOM nodes change
|
|
84
|
+
- Reorder: DOM nodes move without recreation
|
|
85
|
+
- Update item: Only that item re-renders
|
|
86
|
+
- Input state preserved during updates
|
|
87
|
+
|
|
88
|
+
### WebSocket Connections
|
|
89
|
+
|
|
90
|
+
Real-time data with declarative WebSocket:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"connections": {
|
|
95
|
+
"chat": {
|
|
96
|
+
"type": "websocket",
|
|
97
|
+
"url": "wss://api.example.com/ws",
|
|
98
|
+
"onMessage": { "action": "handleMessage" },
|
|
99
|
+
"onOpen": { "action": "connectionOpened" },
|
|
100
|
+
"onClose": { "action": "connectionClosed" }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Send messages:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "do": "send", "connection": "chat", "data": { "expr": "state", "name": "inputText" } }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Close connection:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "do": "close", "connection": "chat" }
|
|
116
|
+
```
|
|
117
|
+
|
|
43
118
|
### Markdown Rendering
|
|
44
119
|
|
|
45
120
|
```json
|
|
@@ -148,17 +223,43 @@ interface AppInstance {
|
|
|
148
223
|
### Reactive Primitives
|
|
149
224
|
|
|
150
225
|
```typescript
|
|
151
|
-
import { createSignal, createEffect } from '@constela/runtime';
|
|
226
|
+
import { createSignal, createEffect, createComputed } from '@constela/runtime';
|
|
152
227
|
|
|
153
228
|
const count = createSignal(0);
|
|
154
229
|
count.get(); // Read
|
|
155
230
|
count.set(1); // Write
|
|
156
231
|
|
|
232
|
+
// Computed values with automatic dependency tracking
|
|
233
|
+
const doubled = createComputed(() => count.get() * 2);
|
|
234
|
+
doubled.get(); // Returns memoized value
|
|
235
|
+
|
|
157
236
|
const cleanup = createEffect(() => {
|
|
158
237
|
console.log(`Count: ${count.get()}`);
|
|
159
238
|
});
|
|
160
239
|
```
|
|
161
240
|
|
|
241
|
+
### TypedStateStore (TypeScript)
|
|
242
|
+
|
|
243
|
+
Type-safe state access for TypeScript developers:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { createTypedStateStore } from '@constela/runtime';
|
|
247
|
+
|
|
248
|
+
interface AppState {
|
|
249
|
+
posts: { id: number; liked: boolean }[];
|
|
250
|
+
filter: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const state = createTypedStateStore<AppState>({
|
|
254
|
+
posts: { type: 'list', initial: [] },
|
|
255
|
+
filter: { type: 'string', initial: '' },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
state.get('posts'); // Type: { id: number; liked: boolean }[]
|
|
259
|
+
state.set('filter', 'recent'); // OK
|
|
260
|
+
state.set('filter', 123); // TypeScript error
|
|
261
|
+
```
|
|
262
|
+
|
|
162
263
|
## License
|
|
163
264
|
|
|
164
265
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,26 @@ type CleanupFn = () => void;
|
|
|
17
17
|
type EffectFn = () => void | CleanupFn;
|
|
18
18
|
declare function createEffect(fn: EffectFn): () => void;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Computed - Derived reactive value
|
|
22
|
+
*
|
|
23
|
+
* A Computed holds a derived value that is automatically recalculated
|
|
24
|
+
* when its dependencies change. It tracks Signal dependencies and
|
|
25
|
+
* memoizes results for efficiency.
|
|
26
|
+
*/
|
|
27
|
+
interface Computed<T> {
|
|
28
|
+
get(): T;
|
|
29
|
+
subscribe?(fn: (value: T) => void): () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a computed value that automatically tracks dependencies
|
|
33
|
+
* and memoizes results.
|
|
34
|
+
*
|
|
35
|
+
* @param getter - Function that computes the derived value
|
|
36
|
+
* @returns Computed object with get() and subscribe() methods
|
|
37
|
+
*/
|
|
38
|
+
declare function createComputed<T>(getter: () => T): Computed<T>;
|
|
39
|
+
|
|
20
40
|
/**
|
|
21
41
|
* StateStore - Centralized state management
|
|
22
42
|
*
|
|
@@ -27,13 +47,62 @@ interface StateStore {
|
|
|
27
47
|
get(name: string): unknown;
|
|
28
48
|
set(name: string, value: unknown): void;
|
|
29
49
|
subscribe(name: string, fn: (value: unknown) => void): () => void;
|
|
50
|
+
getPath(name: string, path: string | (string | number)[]): unknown;
|
|
51
|
+
setPath(name: string, path: string | (string | number)[], value: unknown): void;
|
|
52
|
+
subscribeToPath(name: string, path: string | (string | number)[], fn: (value: unknown) => void): () => void;
|
|
30
53
|
}
|
|
31
54
|
interface StateDefinition {
|
|
32
55
|
type: string;
|
|
33
56
|
initial: unknown;
|
|
34
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* TypedStateStore - Generic interface for type-safe state access
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* interface AppState {
|
|
63
|
+
* items: { id: number; liked: boolean }[];
|
|
64
|
+
* filter: string;
|
|
65
|
+
* }
|
|
66
|
+
* const state = createStateStore(definitions) as TypedStateStore<AppState>;
|
|
67
|
+
* state.get('items'); // returns { id: number; liked: boolean }[]
|
|
68
|
+
*/
|
|
69
|
+
interface TypedStateStore<T extends Record<string, unknown>> extends StateStore {
|
|
70
|
+
get<K extends keyof T>(name: K): T[K];
|
|
71
|
+
set<K extends keyof T>(name: K, value: T[K]): void;
|
|
72
|
+
subscribe<K extends keyof T>(name: K, fn: (value: T[K]) => void): () => void;
|
|
73
|
+
getPath<K extends keyof T>(name: K, path: string | (string | number)[]): unknown;
|
|
74
|
+
setPath<K extends keyof T>(name: K, path: string | (string | number)[], value: unknown): void;
|
|
75
|
+
subscribeToPath<K extends keyof T>(name: K, path: string | (string | number)[], fn: (value: unknown) => void): () => void;
|
|
76
|
+
}
|
|
35
77
|
declare function createStateStore(definitions: Record<string, StateDefinition>): StateStore;
|
|
36
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Typed State Store - Helper for creating type-safe state stores
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a type-safe state store with inferred types
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* interface AppState {
|
|
88
|
+
* items: { id: number; liked: boolean }[];
|
|
89
|
+
* filter: string;
|
|
90
|
+
* count: number;
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* const state = createTypedStateStore<AppState>({
|
|
94
|
+
* items: { type: 'list', initial: [] },
|
|
95
|
+
* filter: { type: 'string', initial: '' },
|
|
96
|
+
* count: { type: 'number', initial: 0 },
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* state.get('items'); // correctly typed as { id: number; liked: boolean }[]
|
|
100
|
+
* state.set('count', 10); // type-checked
|
|
101
|
+
*/
|
|
102
|
+
declare function createTypedStateStore<T extends Record<string, unknown>>(definitions: {
|
|
103
|
+
[K in keyof T]: StateDefinition;
|
|
104
|
+
}): TypedStateStore<T>;
|
|
105
|
+
|
|
37
106
|
/**
|
|
38
107
|
* Expression Evaluator - Evaluates compiled expressions
|
|
39
108
|
*
|
|
@@ -88,6 +157,91 @@ interface StyleExprInput {
|
|
|
88
157
|
*/
|
|
89
158
|
declare function evaluateStyle(expr: StyleExprInput, ctx: EvaluationContext): string | undefined;
|
|
90
159
|
|
|
160
|
+
/**
|
|
161
|
+
* WebSocket Connection Module - Interface Stubs for TDD
|
|
162
|
+
*
|
|
163
|
+
* This file provides the interface definitions and stub implementations
|
|
164
|
+
* for WebSocket connection management in Constela runtime.
|
|
165
|
+
*
|
|
166
|
+
* TDD Red Phase: These stubs will fail tests until properly implemented.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* WebSocket connection interface for sending/receiving data
|
|
170
|
+
*/
|
|
171
|
+
interface WebSocketConnection {
|
|
172
|
+
/**
|
|
173
|
+
* Send data through the WebSocket connection
|
|
174
|
+
* Objects and arrays are JSON stringified
|
|
175
|
+
* @param data - The data to send
|
|
176
|
+
*/
|
|
177
|
+
send(data: unknown): void;
|
|
178
|
+
/**
|
|
179
|
+
* Close the WebSocket connection
|
|
180
|
+
*/
|
|
181
|
+
close(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Get the current connection state
|
|
184
|
+
* @returns The connection state
|
|
185
|
+
*/
|
|
186
|
+
getState(): 'connecting' | 'open' | 'closing' | 'closed';
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Event handlers for WebSocket connection events
|
|
190
|
+
*/
|
|
191
|
+
interface WebSocketHandlers {
|
|
192
|
+
/** Called when connection is established */
|
|
193
|
+
onOpen?: () => Promise<void> | void;
|
|
194
|
+
/** Called when connection is closed */
|
|
195
|
+
onClose?: (code: number, reason: string) => Promise<void> | void;
|
|
196
|
+
/** Called when an error occurs */
|
|
197
|
+
onError?: (error: Event) => Promise<void> | void;
|
|
198
|
+
/** Called when a message is received (JSON parsed if possible) */
|
|
199
|
+
onMessage?: (data: unknown) => Promise<void> | void;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Connection manager for named WebSocket connections
|
|
203
|
+
*/
|
|
204
|
+
interface ConnectionManager {
|
|
205
|
+
/**
|
|
206
|
+
* Create a new named WebSocket connection
|
|
207
|
+
* If a connection with the same name exists, it will be closed first
|
|
208
|
+
*/
|
|
209
|
+
create(name: string, url: string, handlers: WebSocketHandlers): void;
|
|
210
|
+
/**
|
|
211
|
+
* Get a connection by name
|
|
212
|
+
* @returns The connection or undefined if not found
|
|
213
|
+
*/
|
|
214
|
+
get(name: string): WebSocketConnection | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Send data to a named connection
|
|
217
|
+
* @throws Error if connection not found
|
|
218
|
+
*/
|
|
219
|
+
send(name: string, data: unknown): void;
|
|
220
|
+
/**
|
|
221
|
+
* Close a named connection
|
|
222
|
+
* No-op if connection not found
|
|
223
|
+
*/
|
|
224
|
+
close(name: string): void;
|
|
225
|
+
/**
|
|
226
|
+
* Close all connections
|
|
227
|
+
*/
|
|
228
|
+
closeAll(): void;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Create a WebSocket connection with event handlers
|
|
232
|
+
*
|
|
233
|
+
* @param url - The WebSocket URL (e.g., "wss://api.example.com/ws")
|
|
234
|
+
* @param handlers - Event handlers for connection lifecycle
|
|
235
|
+
* @returns A WebSocketConnection interface for sending/closing
|
|
236
|
+
*/
|
|
237
|
+
declare function createWebSocketConnection(url: string, handlers: WebSocketHandlers): WebSocketConnection;
|
|
238
|
+
/**
|
|
239
|
+
* Create a connection manager for managing multiple named connections
|
|
240
|
+
*
|
|
241
|
+
* @returns A ConnectionManager interface
|
|
242
|
+
*/
|
|
243
|
+
declare function createConnectionManager(): ConnectionManager;
|
|
244
|
+
|
|
91
245
|
/**
|
|
92
246
|
* Action Executor - Executes compiled action steps
|
|
93
247
|
*
|
|
@@ -113,6 +267,7 @@ interface ActionContext {
|
|
|
113
267
|
path: string;
|
|
114
268
|
};
|
|
115
269
|
imports?: Record<string, unknown>;
|
|
270
|
+
connections?: ConnectionManager;
|
|
116
271
|
}
|
|
117
272
|
declare function executeAction(action: CompiledAction, ctx: ActionContext): Promise<void>;
|
|
118
273
|
|
|
@@ -203,4 +358,4 @@ interface HydrateOptions {
|
|
|
203
358
|
*/
|
|
204
359
|
declare function hydrateApp(options: HydrateOptions): AppInstance;
|
|
205
360
|
|
|
206
|
-
export { type ActionContext, type AppInstance, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, type StylePreset, createApp, createEffect, createSignal, createStateStore, evaluate, evaluateStyle, executeAction, hydrateApp, render };
|
|
361
|
+
export { type ActionContext, type AppInstance, type Computed, type ConnectionManager, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, type StylePreset, type TypedStateStore, type WebSocketConnection, type WebSocketHandlers, createApp, createComputed, createConnectionManager, createEffect, createSignal, createStateStore, createTypedStateStore, createWebSocketConnection, evaluate, evaluateStyle, executeAction, hydrateApp, render };
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ var effectDependencies = /* @__PURE__ */ new Map();
|
|
|
9
9
|
function setCurrentEffect(effect) {
|
|
10
10
|
currentEffect = effect;
|
|
11
11
|
}
|
|
12
|
+
function getCurrentEffect() {
|
|
13
|
+
return currentEffect;
|
|
14
|
+
}
|
|
12
15
|
function registerEffectCleanup(effect) {
|
|
13
16
|
if (!effectDependencies.has(effect)) {
|
|
14
17
|
effectDependencies.set(effect, /* @__PURE__ */ new Set());
|
|
@@ -95,7 +98,121 @@ function createEffect(fn) {
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
// src/reactive/computed.ts
|
|
102
|
+
function createComputed(getter) {
|
|
103
|
+
let cachedValue;
|
|
104
|
+
let isDirty = true;
|
|
105
|
+
let isComputing = false;
|
|
106
|
+
let hasValue = false;
|
|
107
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
108
|
+
const effectSubscribers = /* @__PURE__ */ new Set();
|
|
109
|
+
const markDirty = () => {
|
|
110
|
+
if (!isDirty) {
|
|
111
|
+
isDirty = true;
|
|
112
|
+
const effects = [...effectSubscribers];
|
|
113
|
+
effects.forEach((effect) => effect());
|
|
114
|
+
if (subscribers.size > 0 && hasValue) {
|
|
115
|
+
const oldValue = cachedValue;
|
|
116
|
+
try {
|
|
117
|
+
compute();
|
|
118
|
+
if (!Object.is(cachedValue, oldValue)) {
|
|
119
|
+
notifySubscribers();
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const compute = () => {
|
|
127
|
+
if (isComputing) {
|
|
128
|
+
throw new Error("Circular dependency detected in computed");
|
|
129
|
+
}
|
|
130
|
+
cleanupEffect(markDirty);
|
|
131
|
+
isComputing = true;
|
|
132
|
+
const previousEffect = getCurrentEffect();
|
|
133
|
+
registerEffectCleanup(markDirty);
|
|
134
|
+
setCurrentEffect(markDirty);
|
|
135
|
+
try {
|
|
136
|
+
cachedValue = getter();
|
|
137
|
+
isDirty = false;
|
|
138
|
+
hasValue = true;
|
|
139
|
+
} finally {
|
|
140
|
+
isComputing = false;
|
|
141
|
+
setCurrentEffect(previousEffect);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const notifySubscribers = () => {
|
|
145
|
+
subscribers.forEach((fn) => {
|
|
146
|
+
try {
|
|
147
|
+
fn(cachedValue);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error("Error in computed subscriber:", e);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
get() {
|
|
155
|
+
if (isDirty) {
|
|
156
|
+
compute();
|
|
157
|
+
}
|
|
158
|
+
const currentEff = getCurrentEffect();
|
|
159
|
+
if (currentEff && currentEff !== markDirty) {
|
|
160
|
+
effectSubscribers.add(currentEff);
|
|
161
|
+
}
|
|
162
|
+
return cachedValue;
|
|
163
|
+
},
|
|
164
|
+
subscribe(fn) {
|
|
165
|
+
if (isDirty) {
|
|
166
|
+
try {
|
|
167
|
+
compute();
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
subscribers.add(fn);
|
|
172
|
+
return () => {
|
|
173
|
+
subscribers.delete(fn);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
98
179
|
// src/state/store.ts
|
|
180
|
+
function normalizePath(path) {
|
|
181
|
+
if (typeof path === "string") {
|
|
182
|
+
return path.split(".").map((segment) => {
|
|
183
|
+
const num = parseInt(segment, 10);
|
|
184
|
+
return isNaN(num) ? segment : num;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return path;
|
|
188
|
+
}
|
|
189
|
+
function getValueAtPath(obj, path) {
|
|
190
|
+
let current = obj;
|
|
191
|
+
for (const key2 of path) {
|
|
192
|
+
if (current == null) return void 0;
|
|
193
|
+
current = current[key2];
|
|
194
|
+
}
|
|
195
|
+
return current;
|
|
196
|
+
}
|
|
197
|
+
function setValueAtPath(obj, path, value) {
|
|
198
|
+
if (path.length === 0) return value;
|
|
199
|
+
const head2 = path[0];
|
|
200
|
+
const rest = path.slice(1);
|
|
201
|
+
const isArrayIndex = typeof head2 === "number";
|
|
202
|
+
let clone3;
|
|
203
|
+
if (isArrayIndex) {
|
|
204
|
+
clone3 = Array.isArray(obj) ? [...obj] : [];
|
|
205
|
+
} else {
|
|
206
|
+
clone3 = obj != null && typeof obj === "object" ? { ...obj } : {};
|
|
207
|
+
}
|
|
208
|
+
const objRecord = obj;
|
|
209
|
+
clone3[head2] = setValueAtPath(
|
|
210
|
+
objRecord?.[head2],
|
|
211
|
+
rest,
|
|
212
|
+
value
|
|
213
|
+
);
|
|
214
|
+
return clone3;
|
|
215
|
+
}
|
|
99
216
|
function createStateStore(definitions) {
|
|
100
217
|
const signals = /* @__PURE__ */ new Map();
|
|
101
218
|
for (const [name, def] of Object.entries(definitions)) {
|
|
@@ -136,10 +253,48 @@ function createStateStore(definitions) {
|
|
|
136
253
|
throw new Error(`State field "${name}" does not exist`);
|
|
137
254
|
}
|
|
138
255
|
return signal.subscribe(fn);
|
|
256
|
+
},
|
|
257
|
+
getPath(name, path) {
|
|
258
|
+
const signal = signals.get(name);
|
|
259
|
+
if (!signal) {
|
|
260
|
+
throw new Error(`State field "${name}" does not exist`);
|
|
261
|
+
}
|
|
262
|
+
const normalizedPath = normalizePath(path);
|
|
263
|
+
return getValueAtPath(signal.get(), normalizedPath);
|
|
264
|
+
},
|
|
265
|
+
setPath(name, path, value) {
|
|
266
|
+
const signal = signals.get(name);
|
|
267
|
+
if (!signal) {
|
|
268
|
+
throw new Error(`State field "${name}" does not exist`);
|
|
269
|
+
}
|
|
270
|
+
const normalizedPath = normalizePath(path);
|
|
271
|
+
const currentState = signal.get();
|
|
272
|
+
const newState = setValueAtPath(currentState, normalizedPath, value);
|
|
273
|
+
signal.set(newState);
|
|
274
|
+
},
|
|
275
|
+
subscribeToPath(name, path, fn) {
|
|
276
|
+
const signal = signals.get(name);
|
|
277
|
+
if (!signal) {
|
|
278
|
+
throw new Error(`State field "${name}" does not exist`);
|
|
279
|
+
}
|
|
280
|
+
const normalizedPath = normalizePath(path);
|
|
281
|
+
let previousValue = getValueAtPath(signal.get(), normalizedPath);
|
|
282
|
+
return signal.subscribe((newFieldValue) => {
|
|
283
|
+
const newValue = getValueAtPath(newFieldValue, normalizedPath);
|
|
284
|
+
if (newValue !== previousValue) {
|
|
285
|
+
previousValue = newValue;
|
|
286
|
+
fn(newValue);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
139
289
|
}
|
|
140
290
|
};
|
|
141
291
|
}
|
|
142
292
|
|
|
293
|
+
// src/state/typed.ts
|
|
294
|
+
function createTypedStateStore(definitions) {
|
|
295
|
+
return createStateStore(definitions);
|
|
296
|
+
}
|
|
297
|
+
|
|
143
298
|
// src/expression/evaluator.ts
|
|
144
299
|
function evaluate(expr, ctx) {
|
|
145
300
|
switch (expr.expr) {
|
|
@@ -267,12 +422,40 @@ function evaluate(expr, ctx) {
|
|
|
267
422
|
}
|
|
268
423
|
case "style":
|
|
269
424
|
return evaluateStyle(expr, ctx);
|
|
425
|
+
case "concat": {
|
|
426
|
+
return expr.items.map((item) => {
|
|
427
|
+
const val = evaluate(item, ctx);
|
|
428
|
+
return val == null ? "" : String(val);
|
|
429
|
+
}).join("");
|
|
430
|
+
}
|
|
270
431
|
default: {
|
|
271
432
|
const _exhaustiveCheck = expr;
|
|
272
433
|
throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
|
|
273
434
|
}
|
|
274
435
|
}
|
|
275
436
|
}
|
|
437
|
+
function isExpression(value) {
|
|
438
|
+
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "expr") && typeof value.expr === "string";
|
|
439
|
+
}
|
|
440
|
+
function evaluatePayload(payload, ctx) {
|
|
441
|
+
if (isExpression(payload)) {
|
|
442
|
+
return evaluate(payload, ctx);
|
|
443
|
+
}
|
|
444
|
+
if (typeof payload === "object" && payload !== null) {
|
|
445
|
+
const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
446
|
+
const result = {};
|
|
447
|
+
for (const [key2, value] of Object.entries(payload)) {
|
|
448
|
+
if (forbiddenKeys.has(key2)) continue;
|
|
449
|
+
if (isExpression(value)) {
|
|
450
|
+
result[key2] = evaluate(value, ctx);
|
|
451
|
+
} else {
|
|
452
|
+
result[key2] = value;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
return payload;
|
|
458
|
+
}
|
|
276
459
|
function getNestedValue(obj, path) {
|
|
277
460
|
const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
278
461
|
const parts = path.split(".");
|
|
@@ -408,7 +591,7 @@ function createEvalContext(ctx) {
|
|
|
408
591
|
}
|
|
409
592
|
async function executeAction(action, ctx) {
|
|
410
593
|
for (const step of action.steps) {
|
|
411
|
-
if (step.do === "set" || step.do === "update") {
|
|
594
|
+
if (step.do === "set" || step.do === "update" || step.do === "setPath") {
|
|
412
595
|
executeStepSync(step, ctx);
|
|
413
596
|
} else if (step.do === "if") {
|
|
414
597
|
await executeIfStep(step, ctx);
|
|
@@ -425,6 +608,9 @@ function executeStepSync(step, ctx) {
|
|
|
425
608
|
case "update":
|
|
426
609
|
executeUpdateStepSync(step, ctx);
|
|
427
610
|
break;
|
|
611
|
+
case "setPath":
|
|
612
|
+
executeSetPathStepSync(step, ctx);
|
|
613
|
+
break;
|
|
428
614
|
}
|
|
429
615
|
}
|
|
430
616
|
async function executeIfStep(step, ctx) {
|
|
@@ -432,7 +618,7 @@ async function executeIfStep(step, ctx) {
|
|
|
432
618
|
const condition = evaluate(step.condition, evalCtx);
|
|
433
619
|
const stepsToExecute = condition ? step.then : step.else || [];
|
|
434
620
|
for (const nestedStep of stepsToExecute) {
|
|
435
|
-
if (nestedStep.do === "set" || nestedStep.do === "update") {
|
|
621
|
+
if (nestedStep.do === "set" || nestedStep.do === "update" || nestedStep.do === "setPath") {
|
|
436
622
|
executeStepSync(nestedStep, ctx);
|
|
437
623
|
} else if (nestedStep.do === "if") {
|
|
438
624
|
await executeIfStep(nestedStep, ctx);
|
|
@@ -532,6 +718,31 @@ function executeUpdateStepSync(step, ctx) {
|
|
|
532
718
|
}
|
|
533
719
|
}
|
|
534
720
|
}
|
|
721
|
+
function executeSetPathStepSync(step, ctx) {
|
|
722
|
+
const evalCtx = createEvalContext(ctx);
|
|
723
|
+
const pathValue = evaluate(step.path, evalCtx);
|
|
724
|
+
let path;
|
|
725
|
+
if (typeof pathValue === "string") {
|
|
726
|
+
path = pathValue.split(".").map((segment) => {
|
|
727
|
+
const num = parseInt(segment, 10);
|
|
728
|
+
return isNaN(num) ? segment : num;
|
|
729
|
+
});
|
|
730
|
+
} else if (Array.isArray(pathValue)) {
|
|
731
|
+
path = pathValue.map((item) => {
|
|
732
|
+
if (typeof item === "object" && item !== null && "expr" in item) {
|
|
733
|
+
return evaluate(item, evalCtx);
|
|
734
|
+
}
|
|
735
|
+
return item;
|
|
736
|
+
});
|
|
737
|
+
} else {
|
|
738
|
+
path = [pathValue];
|
|
739
|
+
}
|
|
740
|
+
const newValue = evaluate(step.value, evalCtx);
|
|
741
|
+
ctx.state.setPath(step.target, path, newValue);
|
|
742
|
+
}
|
|
743
|
+
async function executeSetPathStep(step, ctx) {
|
|
744
|
+
executeSetPathStepSync(step, ctx);
|
|
745
|
+
}
|
|
535
746
|
async function executeStep(step, ctx) {
|
|
536
747
|
switch (step.do) {
|
|
537
748
|
case "set":
|
|
@@ -540,6 +751,9 @@ async function executeStep(step, ctx) {
|
|
|
540
751
|
case "update":
|
|
541
752
|
await executeUpdateStep(step, ctx);
|
|
542
753
|
break;
|
|
754
|
+
case "setPath":
|
|
755
|
+
await executeSetPathStep(step, ctx);
|
|
756
|
+
break;
|
|
543
757
|
case "fetch":
|
|
544
758
|
await executeFetchStep(step, ctx);
|
|
545
759
|
break;
|
|
@@ -570,6 +784,12 @@ async function executeStep(step, ctx) {
|
|
|
570
784
|
case "if":
|
|
571
785
|
await executeIfStep(step, ctx);
|
|
572
786
|
break;
|
|
787
|
+
case "send":
|
|
788
|
+
await executeSendStep(step, ctx);
|
|
789
|
+
break;
|
|
790
|
+
case "close":
|
|
791
|
+
await executeCloseStep(step, ctx);
|
|
792
|
+
break;
|
|
573
793
|
}
|
|
574
794
|
}
|
|
575
795
|
async function executeSetStep(target, value, ctx) {
|
|
@@ -924,6 +1144,18 @@ async function executeDomStep(step, ctx) {
|
|
|
924
1144
|
break;
|
|
925
1145
|
}
|
|
926
1146
|
}
|
|
1147
|
+
async function executeSendStep(step, ctx) {
|
|
1148
|
+
if (!ctx.connections) {
|
|
1149
|
+
throw new Error(`Connection "${step.connection}" not found`);
|
|
1150
|
+
}
|
|
1151
|
+
const evalCtx = createEvalContext(ctx);
|
|
1152
|
+
const data = evaluate(step.data, evalCtx);
|
|
1153
|
+
ctx.connections.send(step.connection, data);
|
|
1154
|
+
}
|
|
1155
|
+
async function executeCloseStep(step, ctx) {
|
|
1156
|
+
if (!ctx.connections) return;
|
|
1157
|
+
ctx.connections.close(step.connection);
|
|
1158
|
+
}
|
|
927
1159
|
|
|
928
1160
|
// ../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js
|
|
929
1161
|
function L() {
|
|
@@ -12961,7 +13193,7 @@ function renderElement(node, ctx) {
|
|
|
12961
13193
|
}
|
|
12962
13194
|
let payload = void 0;
|
|
12963
13195
|
if (handler.payload) {
|
|
12964
|
-
payload =
|
|
13196
|
+
payload = evaluatePayload(handler.payload, {
|
|
12965
13197
|
state: ctx.state,
|
|
12966
13198
|
locals: { ...ctx.locals, ...eventLocals },
|
|
12967
13199
|
...ctx.imports && { imports: ctx.imports }
|
|
@@ -13095,52 +13327,162 @@ function renderIf(node, ctx) {
|
|
|
13095
13327
|
}
|
|
13096
13328
|
return fragment;
|
|
13097
13329
|
}
|
|
13330
|
+
function createReactiveLocals(baseLocals, itemKey, itemSignal, indexKey, indexSignal) {
|
|
13331
|
+
return new Proxy(baseLocals, {
|
|
13332
|
+
get(target, prop) {
|
|
13333
|
+
if (prop === itemKey) {
|
|
13334
|
+
return itemSignal.get();
|
|
13335
|
+
}
|
|
13336
|
+
if (indexKey && prop === indexKey) {
|
|
13337
|
+
return indexSignal.get();
|
|
13338
|
+
}
|
|
13339
|
+
return target[prop];
|
|
13340
|
+
},
|
|
13341
|
+
has(target, prop) {
|
|
13342
|
+
if (prop === itemKey) return true;
|
|
13343
|
+
if (indexKey && prop === indexKey) return true;
|
|
13344
|
+
return prop in target;
|
|
13345
|
+
}
|
|
13346
|
+
});
|
|
13347
|
+
}
|
|
13098
13348
|
function renderEach(node, ctx) {
|
|
13099
13349
|
const anchor = document.createComment("each");
|
|
13350
|
+
const hasKey = !!node.key;
|
|
13351
|
+
let itemStateMap = /* @__PURE__ */ new Map();
|
|
13100
13352
|
let currentNodes = [];
|
|
13101
13353
|
let itemCleanups = [];
|
|
13102
13354
|
const effectCleanup = createEffect(() => {
|
|
13103
13355
|
const items = evaluate(node.items, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
|
|
13104
|
-
|
|
13105
|
-
cleanup
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
oldNode.parentNode
|
|
13356
|
+
if (!hasKey || !node.key) {
|
|
13357
|
+
for (const cleanup of itemCleanups) {
|
|
13358
|
+
cleanup();
|
|
13359
|
+
}
|
|
13360
|
+
itemCleanups = [];
|
|
13361
|
+
for (const oldNode of currentNodes) {
|
|
13362
|
+
if (oldNode.parentNode) {
|
|
13363
|
+
oldNode.parentNode.removeChild(oldNode);
|
|
13364
|
+
}
|
|
13111
13365
|
}
|
|
13366
|
+
currentNodes = [];
|
|
13367
|
+
if (Array.isArray(items)) {
|
|
13368
|
+
items.forEach((item, index) => {
|
|
13369
|
+
const itemLocals = {
|
|
13370
|
+
...ctx.locals,
|
|
13371
|
+
[node.as]: item
|
|
13372
|
+
};
|
|
13373
|
+
if (node.index) {
|
|
13374
|
+
itemLocals[node.index] = index;
|
|
13375
|
+
}
|
|
13376
|
+
const localCleanups = [];
|
|
13377
|
+
const itemCtx = {
|
|
13378
|
+
...ctx,
|
|
13379
|
+
locals: itemLocals,
|
|
13380
|
+
cleanups: localCleanups
|
|
13381
|
+
};
|
|
13382
|
+
const itemNode = render(node.body, itemCtx);
|
|
13383
|
+
currentNodes.push(itemNode);
|
|
13384
|
+
itemCleanups.push(...localCleanups);
|
|
13385
|
+
if (anchor.parentNode) {
|
|
13386
|
+
let refNode = anchor.nextSibling;
|
|
13387
|
+
if (currentNodes.length > 1) {
|
|
13388
|
+
const lastExisting = currentNodes[currentNodes.length - 2];
|
|
13389
|
+
if (lastExisting) {
|
|
13390
|
+
refNode = lastExisting.nextSibling;
|
|
13391
|
+
}
|
|
13392
|
+
}
|
|
13393
|
+
anchor.parentNode.insertBefore(itemNode, refNode);
|
|
13394
|
+
}
|
|
13395
|
+
});
|
|
13396
|
+
}
|
|
13397
|
+
return;
|
|
13112
13398
|
}
|
|
13113
|
-
|
|
13399
|
+
const newItemStateMap = /* @__PURE__ */ new Map();
|
|
13400
|
+
const newNodes = [];
|
|
13401
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
13114
13402
|
if (Array.isArray(items)) {
|
|
13115
13403
|
items.forEach((item, index) => {
|
|
13116
|
-
const
|
|
13404
|
+
const tempLocals = {
|
|
13117
13405
|
...ctx.locals,
|
|
13118
|
-
[node.as]: item
|
|
13119
|
-
|
|
13120
|
-
if (node.index) {
|
|
13121
|
-
itemLocals[node.index] = index;
|
|
13122
|
-
}
|
|
13123
|
-
const localCleanups = [];
|
|
13124
|
-
const itemCtx = {
|
|
13125
|
-
...ctx,
|
|
13126
|
-
locals: itemLocals,
|
|
13127
|
-
cleanups: localCleanups
|
|
13406
|
+
[node.as]: item,
|
|
13407
|
+
...node.index ? { [node.index]: index } : {}
|
|
13128
13408
|
};
|
|
13129
|
-
const
|
|
13130
|
-
|
|
13131
|
-
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13136
|
-
|
|
13137
|
-
refNode = lastExisting.nextSibling;
|
|
13138
|
-
}
|
|
13409
|
+
const keyValue = evaluate(node.key, {
|
|
13410
|
+
state: ctx.state,
|
|
13411
|
+
locals: tempLocals,
|
|
13412
|
+
...ctx.imports && { imports: ctx.imports }
|
|
13413
|
+
});
|
|
13414
|
+
if (seenKeys.has(keyValue)) {
|
|
13415
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
13416
|
+
console.warn(`Duplicate key "${keyValue}" in each loop. Keys should be unique.`);
|
|
13139
13417
|
}
|
|
13140
|
-
|
|
13418
|
+
}
|
|
13419
|
+
seenKeys.add(keyValue);
|
|
13420
|
+
const existingState = itemStateMap.get(keyValue);
|
|
13421
|
+
if (existingState) {
|
|
13422
|
+
existingState.itemSignal.set(item);
|
|
13423
|
+
existingState.indexSignal.set(index);
|
|
13424
|
+
newItemStateMap.set(keyValue, existingState);
|
|
13425
|
+
newNodes.push(existingState.node);
|
|
13426
|
+
} else {
|
|
13427
|
+
const itemSignal = createSignal(item);
|
|
13428
|
+
const indexSignal = createSignal(index);
|
|
13429
|
+
const reactiveLocals = createReactiveLocals(
|
|
13430
|
+
ctx.locals,
|
|
13431
|
+
node.as,
|
|
13432
|
+
itemSignal,
|
|
13433
|
+
node.index,
|
|
13434
|
+
indexSignal
|
|
13435
|
+
);
|
|
13436
|
+
const localCleanups = [];
|
|
13437
|
+
const itemCtx = {
|
|
13438
|
+
...ctx,
|
|
13439
|
+
locals: reactiveLocals,
|
|
13440
|
+
cleanups: localCleanups
|
|
13441
|
+
};
|
|
13442
|
+
const itemNode = render(node.body, itemCtx);
|
|
13443
|
+
const newState = {
|
|
13444
|
+
key: keyValue,
|
|
13445
|
+
node: itemNode,
|
|
13446
|
+
cleanups: localCleanups,
|
|
13447
|
+
itemSignal,
|
|
13448
|
+
indexSignal
|
|
13449
|
+
};
|
|
13450
|
+
newItemStateMap.set(keyValue, newState);
|
|
13451
|
+
newNodes.push(itemNode);
|
|
13141
13452
|
}
|
|
13142
13453
|
});
|
|
13143
13454
|
}
|
|
13455
|
+
for (const [key2, state] of itemStateMap) {
|
|
13456
|
+
if (!newItemStateMap.has(key2)) {
|
|
13457
|
+
for (const cleanup of state.cleanups) {
|
|
13458
|
+
cleanup();
|
|
13459
|
+
}
|
|
13460
|
+
if (state.node.parentNode) {
|
|
13461
|
+
state.node.parentNode.removeChild(state.node);
|
|
13462
|
+
}
|
|
13463
|
+
}
|
|
13464
|
+
}
|
|
13465
|
+
const activeElement = document.activeElement;
|
|
13466
|
+
const shouldRestoreFocus = activeElement && activeElement !== document.body;
|
|
13467
|
+
if (anchor.parentNode) {
|
|
13468
|
+
let refNode = anchor;
|
|
13469
|
+
for (const itemNode of newNodes) {
|
|
13470
|
+
const nextSibling = refNode.nextSibling;
|
|
13471
|
+
if (nextSibling !== itemNode) {
|
|
13472
|
+
anchor.parentNode.insertBefore(itemNode, refNode.nextSibling);
|
|
13473
|
+
}
|
|
13474
|
+
refNode = itemNode;
|
|
13475
|
+
}
|
|
13476
|
+
}
|
|
13477
|
+
if (shouldRestoreFocus && activeElement instanceof HTMLElement && document.activeElement !== activeElement) {
|
|
13478
|
+
activeElement.focus();
|
|
13479
|
+
}
|
|
13480
|
+
itemStateMap = newItemStateMap;
|
|
13481
|
+
currentNodes = newNodes;
|
|
13482
|
+
itemCleanups = [];
|
|
13483
|
+
for (const state of itemStateMap.values()) {
|
|
13484
|
+
itemCleanups.push(...state.cleanups);
|
|
13485
|
+
}
|
|
13144
13486
|
});
|
|
13145
13487
|
ctx.cleanups?.push(effectCleanup);
|
|
13146
13488
|
ctx.cleanups?.push(() => {
|
|
@@ -13263,6 +13605,20 @@ function createApp(program, mount) {
|
|
|
13263
13605
|
}
|
|
13264
13606
|
|
|
13265
13607
|
// src/hydrate.ts
|
|
13608
|
+
function createReactiveLocals2(baseLocals, itemSignal, indexSignal, itemName, indexName) {
|
|
13609
|
+
return new Proxy(baseLocals, {
|
|
13610
|
+
get(target, prop) {
|
|
13611
|
+
if (prop === itemName) return itemSignal.get();
|
|
13612
|
+
if (indexName && prop === indexName) return indexSignal.get();
|
|
13613
|
+
return target[prop];
|
|
13614
|
+
},
|
|
13615
|
+
has(target, prop) {
|
|
13616
|
+
if (prop === itemName) return true;
|
|
13617
|
+
if (indexName && prop === indexName) return true;
|
|
13618
|
+
return prop in target;
|
|
13619
|
+
}
|
|
13620
|
+
});
|
|
13621
|
+
}
|
|
13266
13622
|
function isEventHandler2(value) {
|
|
13267
13623
|
return typeof value === "object" && value !== null && "event" in value && "action" in value;
|
|
13268
13624
|
}
|
|
@@ -13383,7 +13739,7 @@ function hydrateElement(node, el, ctx) {
|
|
|
13383
13739
|
}
|
|
13384
13740
|
let payload = void 0;
|
|
13385
13741
|
if (handler.payload) {
|
|
13386
|
-
payload =
|
|
13742
|
+
payload = evaluatePayload(handler.payload, {
|
|
13387
13743
|
state: ctx.state,
|
|
13388
13744
|
locals: { ...ctx.locals, ...eventLocals },
|
|
13389
13745
|
...ctx.imports && { imports: ctx.imports },
|
|
@@ -13693,6 +14049,8 @@ function hydrateEach(node, firstItemDomNode, ctx) {
|
|
|
13693
14049
|
if (!parent) return;
|
|
13694
14050
|
const anchor = document.createComment("each");
|
|
13695
14051
|
parent.insertBefore(anchor, firstItemDomNode);
|
|
14052
|
+
const hasKey = !!node.key;
|
|
14053
|
+
let itemStateMap = /* @__PURE__ */ new Map();
|
|
13696
14054
|
let currentNodes = [];
|
|
13697
14055
|
let itemCleanups = [];
|
|
13698
14056
|
const initialItems = evaluate(node.items, {
|
|
@@ -13704,29 +14062,85 @@ function hydrateEach(node, firstItemDomNode, ctx) {
|
|
|
13704
14062
|
let isFirstRun = true;
|
|
13705
14063
|
if (Array.isArray(initialItems) && initialItems.length > 0) {
|
|
13706
14064
|
let domNode = firstItemDomNode;
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
14065
|
+
if (hasKey && node.key) {
|
|
14066
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
14067
|
+
initialItems.forEach((item, index) => {
|
|
14068
|
+
if (!domNode) return;
|
|
14069
|
+
const tempLocals = {
|
|
14070
|
+
...ctx.locals,
|
|
14071
|
+
[node.as]: item,
|
|
14072
|
+
...node.index ? { [node.index]: index } : {}
|
|
14073
|
+
};
|
|
14074
|
+
const keyValue = evaluate(node.key, {
|
|
14075
|
+
state: ctx.state,
|
|
14076
|
+
locals: tempLocals,
|
|
14077
|
+
...ctx.imports && { imports: ctx.imports },
|
|
14078
|
+
...ctx.route && { route: ctx.route }
|
|
14079
|
+
});
|
|
14080
|
+
if (seenKeys.has(keyValue)) {
|
|
14081
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
14082
|
+
console.warn(`Duplicate key "${keyValue}" in each loop. Keys should be unique.`);
|
|
14083
|
+
}
|
|
14084
|
+
}
|
|
14085
|
+
seenKeys.add(keyValue);
|
|
14086
|
+
const itemSignal = createSignal(item);
|
|
14087
|
+
const indexSignal = createSignal(index);
|
|
14088
|
+
const reactiveLocals = createReactiveLocals2(
|
|
14089
|
+
ctx.locals,
|
|
14090
|
+
itemSignal,
|
|
14091
|
+
indexSignal,
|
|
14092
|
+
node.as,
|
|
14093
|
+
node.index
|
|
14094
|
+
);
|
|
14095
|
+
const localCleanups = [];
|
|
14096
|
+
const itemCtx = {
|
|
14097
|
+
...ctx,
|
|
14098
|
+
locals: reactiveLocals,
|
|
14099
|
+
cleanups: localCleanups
|
|
14100
|
+
};
|
|
14101
|
+
hydrate(node.body, domNode, itemCtx);
|
|
14102
|
+
const itemState = {
|
|
14103
|
+
key: keyValue,
|
|
14104
|
+
node: domNode,
|
|
14105
|
+
cleanups: localCleanups,
|
|
14106
|
+
itemSignal,
|
|
14107
|
+
indexSignal
|
|
14108
|
+
};
|
|
14109
|
+
itemStateMap.set(keyValue, itemState);
|
|
14110
|
+
currentNodes.push(domNode);
|
|
13727
14111
|
domNode = domNode.nextSibling;
|
|
14112
|
+
while (domNode && domNode.nodeType === Node.COMMENT_NODE) {
|
|
14113
|
+
domNode = domNode.nextSibling;
|
|
14114
|
+
}
|
|
14115
|
+
});
|
|
14116
|
+
for (const state of itemStateMap.values()) {
|
|
14117
|
+
itemCleanups.push(...state.cleanups);
|
|
13728
14118
|
}
|
|
13729
|
-
}
|
|
14119
|
+
} else {
|
|
14120
|
+
initialItems.forEach((item, index) => {
|
|
14121
|
+
if (!domNode) return;
|
|
14122
|
+
currentNodes.push(domNode);
|
|
14123
|
+
const itemLocals = {
|
|
14124
|
+
...ctx.locals,
|
|
14125
|
+
[node.as]: item
|
|
14126
|
+
};
|
|
14127
|
+
if (node.index) {
|
|
14128
|
+
itemLocals[node.index] = index;
|
|
14129
|
+
}
|
|
14130
|
+
const localCleanups = [];
|
|
14131
|
+
const itemCtx = {
|
|
14132
|
+
...ctx,
|
|
14133
|
+
locals: itemLocals,
|
|
14134
|
+
cleanups: localCleanups
|
|
14135
|
+
};
|
|
14136
|
+
hydrate(node.body, domNode, itemCtx);
|
|
14137
|
+
itemCleanups.push(...localCleanups);
|
|
14138
|
+
domNode = domNode.nextSibling;
|
|
14139
|
+
while (domNode && domNode.nodeType === Node.COMMENT_NODE) {
|
|
14140
|
+
domNode = domNode.nextSibling;
|
|
14141
|
+
}
|
|
14142
|
+
});
|
|
14143
|
+
}
|
|
13730
14144
|
}
|
|
13731
14145
|
const effectCleanup = createEffect(() => {
|
|
13732
14146
|
const items = evaluate(node.items, {
|
|
@@ -13739,48 +14153,141 @@ function hydrateEach(node, firstItemDomNode, ctx) {
|
|
|
13739
14153
|
isFirstRun = false;
|
|
13740
14154
|
return;
|
|
13741
14155
|
}
|
|
13742
|
-
|
|
13743
|
-
cleanup
|
|
13744
|
-
|
|
13745
|
-
itemCleanups = [];
|
|
13746
|
-
for (const oldNode of currentNodes) {
|
|
13747
|
-
if (oldNode.parentNode) {
|
|
13748
|
-
oldNode.parentNode.removeChild(oldNode);
|
|
14156
|
+
if (!hasKey || !node.key) {
|
|
14157
|
+
for (const cleanup of itemCleanups) {
|
|
14158
|
+
cleanup();
|
|
13749
14159
|
}
|
|
14160
|
+
itemCleanups = [];
|
|
14161
|
+
for (const oldNode of currentNodes) {
|
|
14162
|
+
if (oldNode.parentNode) {
|
|
14163
|
+
oldNode.parentNode.removeChild(oldNode);
|
|
14164
|
+
}
|
|
14165
|
+
}
|
|
14166
|
+
currentNodes = [];
|
|
14167
|
+
if (Array.isArray(items)) {
|
|
14168
|
+
items.forEach((item, index) => {
|
|
14169
|
+
const itemLocals = {
|
|
14170
|
+
...ctx.locals,
|
|
14171
|
+
[node.as]: item
|
|
14172
|
+
};
|
|
14173
|
+
if (node.index) {
|
|
14174
|
+
itemLocals[node.index] = index;
|
|
14175
|
+
}
|
|
14176
|
+
const localCleanups = [];
|
|
14177
|
+
const itemCtx = {
|
|
14178
|
+
state: ctx.state,
|
|
14179
|
+
actions: ctx.actions,
|
|
14180
|
+
locals: itemLocals,
|
|
14181
|
+
cleanups: localCleanups,
|
|
14182
|
+
...ctx.imports && { imports: ctx.imports }
|
|
14183
|
+
};
|
|
14184
|
+
const itemNode = render(node.body, itemCtx);
|
|
14185
|
+
currentNodes.push(itemNode);
|
|
14186
|
+
itemCleanups.push(...localCleanups);
|
|
14187
|
+
if (anchor.parentNode) {
|
|
14188
|
+
let refNode = anchor.nextSibling;
|
|
14189
|
+
if (currentNodes.length > 1) {
|
|
14190
|
+
const lastExisting = currentNodes[currentNodes.length - 2];
|
|
14191
|
+
if (lastExisting) {
|
|
14192
|
+
refNode = lastExisting.nextSibling;
|
|
14193
|
+
}
|
|
14194
|
+
}
|
|
14195
|
+
anchor.parentNode.insertBefore(itemNode, refNode);
|
|
14196
|
+
}
|
|
14197
|
+
});
|
|
14198
|
+
}
|
|
14199
|
+
return;
|
|
13750
14200
|
}
|
|
13751
|
-
|
|
14201
|
+
const newItemStateMap = /* @__PURE__ */ new Map();
|
|
14202
|
+
const newNodes = [];
|
|
14203
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
13752
14204
|
if (Array.isArray(items)) {
|
|
13753
14205
|
items.forEach((item, index) => {
|
|
13754
|
-
const
|
|
14206
|
+
const tempLocals = {
|
|
13755
14207
|
...ctx.locals,
|
|
13756
|
-
[node.as]: item
|
|
14208
|
+
[node.as]: item,
|
|
14209
|
+
...node.index ? { [node.index]: index } : {}
|
|
13757
14210
|
};
|
|
13758
|
-
|
|
13759
|
-
itemLocals[node.index] = index;
|
|
13760
|
-
}
|
|
13761
|
-
const localCleanups = [];
|
|
13762
|
-
const itemCtx = {
|
|
14211
|
+
const keyValue = evaluate(node.key, {
|
|
13763
14212
|
state: ctx.state,
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
itemCleanups.push(...localCleanups);
|
|
13772
|
-
if (anchor.parentNode) {
|
|
13773
|
-
let refNode = anchor.nextSibling;
|
|
13774
|
-
if (currentNodes.length > 1) {
|
|
13775
|
-
const lastExisting = currentNodes[currentNodes.length - 2];
|
|
13776
|
-
if (lastExisting) {
|
|
13777
|
-
refNode = lastExisting.nextSibling;
|
|
13778
|
-
}
|
|
14213
|
+
locals: tempLocals,
|
|
14214
|
+
...ctx.imports && { imports: ctx.imports },
|
|
14215
|
+
...ctx.route && { route: ctx.route }
|
|
14216
|
+
});
|
|
14217
|
+
if (seenKeys.has(keyValue)) {
|
|
14218
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
14219
|
+
console.warn(`Duplicate key "${keyValue}" in each loop. Keys should be unique.`);
|
|
13779
14220
|
}
|
|
13780
|
-
|
|
14221
|
+
}
|
|
14222
|
+
seenKeys.add(keyValue);
|
|
14223
|
+
const existingState = itemStateMap.get(keyValue);
|
|
14224
|
+
if (existingState) {
|
|
14225
|
+
existingState.itemSignal.set(item);
|
|
14226
|
+
existingState.indexSignal.set(index);
|
|
14227
|
+
newItemStateMap.set(keyValue, existingState);
|
|
14228
|
+
newNodes.push(existingState.node);
|
|
14229
|
+
} else {
|
|
14230
|
+
const itemSignal = createSignal(item);
|
|
14231
|
+
const indexSignal = createSignal(index);
|
|
14232
|
+
const reactiveLocals = createReactiveLocals2(
|
|
14233
|
+
ctx.locals,
|
|
14234
|
+
itemSignal,
|
|
14235
|
+
indexSignal,
|
|
14236
|
+
node.as,
|
|
14237
|
+
node.index
|
|
14238
|
+
);
|
|
14239
|
+
const localCleanups = [];
|
|
14240
|
+
const itemCtx = {
|
|
14241
|
+
state: ctx.state,
|
|
14242
|
+
actions: ctx.actions,
|
|
14243
|
+
locals: reactiveLocals,
|
|
14244
|
+
cleanups: localCleanups,
|
|
14245
|
+
...ctx.imports && { imports: ctx.imports }
|
|
14246
|
+
};
|
|
14247
|
+
const itemNode = render(node.body, itemCtx);
|
|
14248
|
+
const newState = {
|
|
14249
|
+
key: keyValue,
|
|
14250
|
+
node: itemNode,
|
|
14251
|
+
cleanups: localCleanups,
|
|
14252
|
+
itemSignal,
|
|
14253
|
+
indexSignal
|
|
14254
|
+
};
|
|
14255
|
+
newItemStateMap.set(keyValue, newState);
|
|
14256
|
+
newNodes.push(itemNode);
|
|
13781
14257
|
}
|
|
13782
14258
|
});
|
|
13783
14259
|
}
|
|
14260
|
+
for (const [key2, state] of itemStateMap) {
|
|
14261
|
+
if (!newItemStateMap.has(key2)) {
|
|
14262
|
+
for (const cleanup of state.cleanups) {
|
|
14263
|
+
cleanup();
|
|
14264
|
+
}
|
|
14265
|
+
if (state.node.parentNode) {
|
|
14266
|
+
state.node.parentNode.removeChild(state.node);
|
|
14267
|
+
}
|
|
14268
|
+
}
|
|
14269
|
+
}
|
|
14270
|
+
const activeElement = document.activeElement;
|
|
14271
|
+
const shouldRestoreFocus = activeElement && activeElement !== document.body;
|
|
14272
|
+
if (anchor.parentNode) {
|
|
14273
|
+
let refNode = anchor;
|
|
14274
|
+
for (const itemNode of newNodes) {
|
|
14275
|
+
const nextSibling = refNode.nextSibling;
|
|
14276
|
+
if (nextSibling !== itemNode) {
|
|
14277
|
+
anchor.parentNode.insertBefore(itemNode, refNode.nextSibling);
|
|
14278
|
+
}
|
|
14279
|
+
refNode = itemNode;
|
|
14280
|
+
}
|
|
14281
|
+
}
|
|
14282
|
+
if (shouldRestoreFocus && activeElement instanceof HTMLElement && document.activeElement !== activeElement) {
|
|
14283
|
+
activeElement.focus();
|
|
14284
|
+
}
|
|
14285
|
+
itemStateMap = newItemStateMap;
|
|
14286
|
+
currentNodes = newNodes;
|
|
14287
|
+
itemCleanups = [];
|
|
14288
|
+
for (const state of itemStateMap.values()) {
|
|
14289
|
+
itemCleanups.push(...state.cleanups);
|
|
14290
|
+
}
|
|
13784
14291
|
});
|
|
13785
14292
|
ctx.cleanups.push(effectCleanup);
|
|
13786
14293
|
ctx.cleanups.push(() => {
|
|
@@ -13809,11 +14316,105 @@ function initCopyButtons(container) {
|
|
|
13809
14316
|
});
|
|
13810
14317
|
});
|
|
13811
14318
|
}
|
|
14319
|
+
|
|
14320
|
+
// src/connection/websocket.ts
|
|
14321
|
+
function createWebSocketConnection(url, handlers) {
|
|
14322
|
+
const ws = new WebSocket(url);
|
|
14323
|
+
ws.onopen = () => {
|
|
14324
|
+
handlers.onOpen?.();
|
|
14325
|
+
};
|
|
14326
|
+
ws.onclose = (event) => {
|
|
14327
|
+
handlers.onClose?.(event.code, event.reason);
|
|
14328
|
+
};
|
|
14329
|
+
ws.onerror = (event) => {
|
|
14330
|
+
handlers.onError?.(event);
|
|
14331
|
+
};
|
|
14332
|
+
ws.onmessage = (event) => {
|
|
14333
|
+
let data = event.data;
|
|
14334
|
+
if (typeof data === "string") {
|
|
14335
|
+
try {
|
|
14336
|
+
data = JSON.parse(data);
|
|
14337
|
+
} catch {
|
|
14338
|
+
}
|
|
14339
|
+
}
|
|
14340
|
+
handlers.onMessage?.(data);
|
|
14341
|
+
};
|
|
14342
|
+
return {
|
|
14343
|
+
send(data) {
|
|
14344
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
14345
|
+
let message;
|
|
14346
|
+
if (typeof data === "string") {
|
|
14347
|
+
message = data;
|
|
14348
|
+
} else {
|
|
14349
|
+
message = JSON.stringify(data);
|
|
14350
|
+
}
|
|
14351
|
+
ws.send(message);
|
|
14352
|
+
}
|
|
14353
|
+
},
|
|
14354
|
+
close() {
|
|
14355
|
+
ws.close();
|
|
14356
|
+
},
|
|
14357
|
+
getState() {
|
|
14358
|
+
switch (ws.readyState) {
|
|
14359
|
+
case WebSocket.CONNECTING:
|
|
14360
|
+
return "connecting";
|
|
14361
|
+
case WebSocket.OPEN:
|
|
14362
|
+
return "open";
|
|
14363
|
+
case WebSocket.CLOSING:
|
|
14364
|
+
return "closing";
|
|
14365
|
+
case WebSocket.CLOSED:
|
|
14366
|
+
return "closed";
|
|
14367
|
+
default:
|
|
14368
|
+
return "closed";
|
|
14369
|
+
}
|
|
14370
|
+
}
|
|
14371
|
+
};
|
|
14372
|
+
}
|
|
14373
|
+
function createConnectionManager() {
|
|
14374
|
+
const connections = /* @__PURE__ */ new Map();
|
|
14375
|
+
return {
|
|
14376
|
+
create(name, url, handlers) {
|
|
14377
|
+
const existing = connections.get(name);
|
|
14378
|
+
if (existing) {
|
|
14379
|
+
existing.close();
|
|
14380
|
+
}
|
|
14381
|
+
const conn = createWebSocketConnection(url, handlers);
|
|
14382
|
+
connections.set(name, conn);
|
|
14383
|
+
},
|
|
14384
|
+
get(name) {
|
|
14385
|
+
return connections.get(name);
|
|
14386
|
+
},
|
|
14387
|
+
send(name, data) {
|
|
14388
|
+
const conn = connections.get(name);
|
|
14389
|
+
if (!conn) {
|
|
14390
|
+
throw new Error(`Connection "${name}" not found`);
|
|
14391
|
+
}
|
|
14392
|
+
conn.send(data);
|
|
14393
|
+
},
|
|
14394
|
+
close(name) {
|
|
14395
|
+
const conn = connections.get(name);
|
|
14396
|
+
if (conn) {
|
|
14397
|
+
conn.close();
|
|
14398
|
+
connections.delete(name);
|
|
14399
|
+
}
|
|
14400
|
+
},
|
|
14401
|
+
closeAll() {
|
|
14402
|
+
for (const conn of connections.values()) {
|
|
14403
|
+
conn.close();
|
|
14404
|
+
}
|
|
14405
|
+
connections.clear();
|
|
14406
|
+
}
|
|
14407
|
+
};
|
|
14408
|
+
}
|
|
13812
14409
|
export {
|
|
13813
14410
|
createApp,
|
|
14411
|
+
createComputed,
|
|
14412
|
+
createConnectionManager,
|
|
13814
14413
|
createEffect,
|
|
13815
14414
|
createSignal,
|
|
13816
14415
|
createStateStore,
|
|
14416
|
+
createTypedStateStore,
|
|
14417
|
+
createWebSocketConnection,
|
|
13817
14418
|
evaluate,
|
|
13818
14419
|
evaluateStyle,
|
|
13819
14420
|
executeAction,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Runtime DOM renderer for Constela UI framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"dompurify": "^3.3.1",
|
|
19
19
|
"marked": "^17.0.1",
|
|
20
20
|
"shiki": "^3.20.0",
|
|
21
|
-
"@constela/
|
|
22
|
-
"@constela/
|
|
21
|
+
"@constela/compiler": "0.9.1",
|
|
22
|
+
"@constela/core": "0.9.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/dompurify": "^3.2.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"tsup": "^8.0.0",
|
|
30
30
|
"typescript": "^5.3.0",
|
|
31
31
|
"vitest": "^2.0.0",
|
|
32
|
-
"@constela/server": "
|
|
32
|
+
"@constela/server": "5.0.1"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=20.0.0"
|