@sola-air-ui/core 1.0.1 → 1.0.2
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/build.js +33 -0
- package/package.json +32 -28
- package/src/index.js +540 -493
package/build.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
mkdirSync('./dist', { recursive: true });
|
|
5
|
+
|
|
6
|
+
await build({
|
|
7
|
+
entryPoints: ['./src/index.js'],
|
|
8
|
+
bundle: true,
|
|
9
|
+
format: 'iife',
|
|
10
|
+
globalName: 'SolaCore',
|
|
11
|
+
outfile: './dist/sola-core.iife.js',
|
|
12
|
+
minify: false,
|
|
13
|
+
target: ['es2020'],
|
|
14
|
+
banner: {
|
|
15
|
+
js: '/* @sola-air-ui/core — IIFE build for ServiceNow and no-bundler environments */'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Also emit a minified version
|
|
20
|
+
await build({
|
|
21
|
+
entryPoints: ['./src/index.js'],
|
|
22
|
+
bundle: true,
|
|
23
|
+
format: 'iife',
|
|
24
|
+
globalName: 'SolaCore',
|
|
25
|
+
outfile: './dist/sola-core.iife.min.js',
|
|
26
|
+
minify: true,
|
|
27
|
+
target: ['es2020'],
|
|
28
|
+
banner: {
|
|
29
|
+
js: '/* @sola-air-ui/core v1.0.2 | MIT */'
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log('Built dist/sola-core.iife.js and dist/sola-core.iife.min.js');
|
package/package.json
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sola-air-ui/core",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Zero-VDOM reactivity engine — signals, effects, lifecycle, and intent primitives",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./src/index.js"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
}
|
|
28
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@sola-air-ui/core",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Zero-VDOM reactivity engine — signals, effects, lifecycle, and intent primitives",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./iife": "./dist/sola-core.iife.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node build.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"sola",
|
|
16
|
+
"reactivity",
|
|
17
|
+
"signals",
|
|
18
|
+
"zero-vdom",
|
|
19
|
+
"ui",
|
|
20
|
+
"frontend"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://sola-air.dev",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/rbm3267/sola-air"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,493 +1,540 @@
|
|
|
1
|
-
// @sola/core — Reactivity Engine V4
|
|
2
|
-
// Milestone 1: Full reactivity primitives, lifecycle hooks, batched updates, configurable intents
|
|
3
|
-
|
|
4
|
-
// ─── Effect Stack ───
|
|
5
|
-
let effectStack = [];
|
|
6
|
-
let pendingEffects = new Set();
|
|
7
|
-
let isFlushing = false;
|
|
8
|
-
|
|
9
|
-
export function flushSync() {
|
|
10
|
-
while (pendingEffects.size > 0) {
|
|
11
|
-
const effects = [...pendingEffects];
|
|
12
|
-
pendingEffects.clear();
|
|
13
|
-
for (const effect of effects) {
|
|
14
|
-
effect.execute();
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
isFlushing = false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ─── Batched Updates ───
|
|
21
|
-
function scheduleFlush() {
|
|
22
|
-
if (!isFlushing) {
|
|
23
|
-
isFlushing = true;
|
|
24
|
-
queueMicrotask(flushSync);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ─── createSignal ───
|
|
29
|
-
export function createSignal(initialValue) {
|
|
30
|
-
let value = initialValue;
|
|
31
|
-
const subscribers = new Set();
|
|
32
|
-
|
|
33
|
-
const read = () => {
|
|
34
|
-
const currentEffect = effectStack[effectStack.length - 1];
|
|
35
|
-
if (currentEffect) {
|
|
36
|
-
subscribers.add(currentEffect);
|
|
37
|
-
currentEffect.dependencies.add(subscribers);
|
|
38
|
-
}
|
|
39
|
-
return value;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const write = (newValue) => {
|
|
43
|
-
if (value !== newValue) {
|
|
44
|
-
value = newValue;
|
|
45
|
-
for (const sub of [...subscribers]) {
|
|
46
|
-
pendingEffects.add(sub);
|
|
47
|
-
}
|
|
48
|
-
scheduleFlush();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return [read, write];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─── createEffect ───
|
|
56
|
-
export function createEffect(fn) {
|
|
57
|
-
const effect = {
|
|
58
|
-
execute() {
|
|
59
|
-
cleanup();
|
|
60
|
-
effectStack.push(effect);
|
|
61
|
-
try {
|
|
62
|
-
fn();
|
|
63
|
-
} finally {
|
|
64
|
-
effectStack.pop();
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
dependencies: new Set(),
|
|
68
|
-
cleanup
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function cleanup() {
|
|
72
|
-
for (const dep of effect.dependencies) {
|
|
73
|
-
dep.delete(effect);
|
|
74
|
-
}
|
|
75
|
-
effect.dependencies.clear();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Run immediately (synchronous initial execution)
|
|
79
|
-
effectStack.push(effect);
|
|
80
|
-
try {
|
|
81
|
-
fn();
|
|
82
|
-
} finally {
|
|
83
|
-
effectStack.pop();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return cleanup;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── createDerived ───
|
|
90
|
-
// Computed signal that auto-tracks dependencies and lazily recomputes.
|
|
91
|
-
export function createDerived(fn) {
|
|
92
|
-
let cachedValue;
|
|
93
|
-
let dirty = true;
|
|
94
|
-
const subscribers = new Set();
|
|
95
|
-
let innerDependencies = new Set();
|
|
96
|
-
|
|
97
|
-
// Track when our dependencies change
|
|
98
|
-
const markDirty = {
|
|
99
|
-
execute() {
|
|
100
|
-
if (!dirty) {
|
|
101
|
-
dirty = true;
|
|
102
|
-
// Propagate to our own subscribers
|
|
103
|
-
for (const sub of [...subscribers]) {
|
|
104
|
-
pendingEffects.add(sub);
|
|
105
|
-
}
|
|
106
|
-
scheduleFlush();
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
dependencies: innerDependencies,
|
|
110
|
-
cleanup() {
|
|
111
|
-
for (const dep of innerDependencies) {
|
|
112
|
-
dep.delete(markDirty);
|
|
113
|
-
}
|
|
114
|
-
innerDependencies.clear();
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const read = () => {
|
|
119
|
-
// Subscribe the current running effect to us
|
|
120
|
-
const currentEffect = effectStack[effectStack.length - 1];
|
|
121
|
-
if (currentEffect) {
|
|
122
|
-
subscribers.add(currentEffect);
|
|
123
|
-
currentEffect.dependencies.add(subscribers);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (dirty) {
|
|
127
|
-
// Clean up old deps
|
|
128
|
-
markDirty.cleanup();
|
|
129
|
-
innerDependencies = new Set();
|
|
130
|
-
markDirty.dependencies = innerDependencies;
|
|
131
|
-
|
|
132
|
-
// Track which signals fn() reads
|
|
133
|
-
effectStack.push(markDirty);
|
|
134
|
-
try {
|
|
135
|
-
cachedValue = fn();
|
|
136
|
-
} finally {
|
|
137
|
-
effectStack.pop();
|
|
138
|
-
}
|
|
139
|
-
dirty = false;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return cachedValue;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
return read;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Component Lifecycle Scope Context Stack ───
|
|
149
|
-
const contextStack = [];
|
|
150
|
-
let activeContext = null;
|
|
151
|
-
|
|
152
|
-
export function pushContext() {
|
|
153
|
-
const ctx = { mounts: [], destroys: [] };
|
|
154
|
-
contextStack.push(ctx);
|
|
155
|
-
activeContext = ctx;
|
|
156
|
-
return ctx;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function popContext(ctx) {
|
|
160
|
-
const idx = contextStack.lastIndexOf(ctx);
|
|
161
|
-
if (idx !== -1) {
|
|
162
|
-
contextStack.splice(idx, 1);
|
|
163
|
-
}
|
|
164
|
-
activeContext = contextStack.length > 0 ? contextStack[contextStack.length - 1] : null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function onMount(fn) {
|
|
168
|
-
if (activeContext) {
|
|
169
|
-
activeContext.mounts.push(fn);
|
|
170
|
-
} else {
|
|
171
|
-
fn();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function onDestroy(fn) {
|
|
176
|
-
if (activeContext) {
|
|
177
|
-
activeContext.destroys.push(fn);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Called by compiled mount() function to flush instance mounts
|
|
182
|
-
export function __flush_mounts() {
|
|
183
|
-
if (activeContext && activeContext.mounts.length > 0) {
|
|
184
|
-
const cbs = [...activeContext.mounts];
|
|
185
|
-
activeContext.mounts = [];
|
|
186
|
-
for (const cb of cbs) {
|
|
187
|
-
cb();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Called when a component is torn down
|
|
193
|
-
export function __flush_destroys() {
|
|
194
|
-
if (activeContext && activeContext.destroys.length > 0) {
|
|
195
|
-
const cbs = [...activeContext.destroys];
|
|
196
|
-
activeContext.destroys = [];
|
|
197
|
-
for (const cb of cbs) {
|
|
198
|
-
cb();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ─── createIntent ───
|
|
204
|
-
// Configurable ambient intent resolver.
|
|
205
|
-
const defaultIntentConfig = {
|
|
206
|
-
provider: 'local',
|
|
207
|
-
endpoint: '/api/intent',
|
|
208
|
-
model: 'gemini-2.5-flash'
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (abortController) abortController.abort();
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (!this.topics.has(name)) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
this.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1
|
+
// @sola/core — Reactivity Engine V4
|
|
2
|
+
// Milestone 1: Full reactivity primitives, lifecycle hooks, batched updates, configurable intents
|
|
3
|
+
|
|
4
|
+
// ─── Effect Stack ───
|
|
5
|
+
let effectStack = [];
|
|
6
|
+
let pendingEffects = new Set();
|
|
7
|
+
let isFlushing = false;
|
|
8
|
+
|
|
9
|
+
export function flushSync() {
|
|
10
|
+
while (pendingEffects.size > 0) {
|
|
11
|
+
const effects = [...pendingEffects];
|
|
12
|
+
pendingEffects.clear();
|
|
13
|
+
for (const effect of effects) {
|
|
14
|
+
effect.execute();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
isFlushing = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Batched Updates ───
|
|
21
|
+
function scheduleFlush() {
|
|
22
|
+
if (!isFlushing) {
|
|
23
|
+
isFlushing = true;
|
|
24
|
+
queueMicrotask(flushSync);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── createSignal ───
|
|
29
|
+
export function createSignal(initialValue) {
|
|
30
|
+
let value = initialValue;
|
|
31
|
+
const subscribers = new Set();
|
|
32
|
+
|
|
33
|
+
const read = () => {
|
|
34
|
+
const currentEffect = effectStack[effectStack.length - 1];
|
|
35
|
+
if (currentEffect) {
|
|
36
|
+
subscribers.add(currentEffect);
|
|
37
|
+
currentEffect.dependencies.add(subscribers);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const write = (newValue) => {
|
|
43
|
+
if (value !== newValue) {
|
|
44
|
+
value = newValue;
|
|
45
|
+
for (const sub of [...subscribers]) {
|
|
46
|
+
pendingEffects.add(sub);
|
|
47
|
+
}
|
|
48
|
+
scheduleFlush();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return [read, write];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── createEffect ───
|
|
56
|
+
export function createEffect(fn) {
|
|
57
|
+
const effect = {
|
|
58
|
+
execute() {
|
|
59
|
+
cleanup();
|
|
60
|
+
effectStack.push(effect);
|
|
61
|
+
try {
|
|
62
|
+
fn();
|
|
63
|
+
} finally {
|
|
64
|
+
effectStack.pop();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
dependencies: new Set(),
|
|
68
|
+
cleanup
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function cleanup() {
|
|
72
|
+
for (const dep of effect.dependencies) {
|
|
73
|
+
dep.delete(effect);
|
|
74
|
+
}
|
|
75
|
+
effect.dependencies.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Run immediately (synchronous initial execution)
|
|
79
|
+
effectStack.push(effect);
|
|
80
|
+
try {
|
|
81
|
+
fn();
|
|
82
|
+
} finally {
|
|
83
|
+
effectStack.pop();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return cleanup;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── createDerived ───
|
|
90
|
+
// Computed signal that auto-tracks dependencies and lazily recomputes.
|
|
91
|
+
export function createDerived(fn) {
|
|
92
|
+
let cachedValue;
|
|
93
|
+
let dirty = true;
|
|
94
|
+
const subscribers = new Set();
|
|
95
|
+
let innerDependencies = new Set();
|
|
96
|
+
|
|
97
|
+
// Track when our dependencies change
|
|
98
|
+
const markDirty = {
|
|
99
|
+
execute() {
|
|
100
|
+
if (!dirty) {
|
|
101
|
+
dirty = true;
|
|
102
|
+
// Propagate to our own subscribers
|
|
103
|
+
for (const sub of [...subscribers]) {
|
|
104
|
+
pendingEffects.add(sub);
|
|
105
|
+
}
|
|
106
|
+
scheduleFlush();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
dependencies: innerDependencies,
|
|
110
|
+
cleanup() {
|
|
111
|
+
for (const dep of innerDependencies) {
|
|
112
|
+
dep.delete(markDirty);
|
|
113
|
+
}
|
|
114
|
+
innerDependencies.clear();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const read = () => {
|
|
119
|
+
// Subscribe the current running effect to us
|
|
120
|
+
const currentEffect = effectStack[effectStack.length - 1];
|
|
121
|
+
if (currentEffect) {
|
|
122
|
+
subscribers.add(currentEffect);
|
|
123
|
+
currentEffect.dependencies.add(subscribers);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (dirty) {
|
|
127
|
+
// Clean up old deps
|
|
128
|
+
markDirty.cleanup();
|
|
129
|
+
innerDependencies = new Set();
|
|
130
|
+
markDirty.dependencies = innerDependencies;
|
|
131
|
+
|
|
132
|
+
// Track which signals fn() reads
|
|
133
|
+
effectStack.push(markDirty);
|
|
134
|
+
try {
|
|
135
|
+
cachedValue = fn();
|
|
136
|
+
} finally {
|
|
137
|
+
effectStack.pop();
|
|
138
|
+
}
|
|
139
|
+
dirty = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return cachedValue;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return read;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Component Lifecycle Scope Context Stack ───
|
|
149
|
+
const contextStack = [];
|
|
150
|
+
let activeContext = null;
|
|
151
|
+
|
|
152
|
+
export function pushContext() {
|
|
153
|
+
const ctx = { mounts: [], destroys: [] };
|
|
154
|
+
contextStack.push(ctx);
|
|
155
|
+
activeContext = ctx;
|
|
156
|
+
return ctx;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function popContext(ctx) {
|
|
160
|
+
const idx = contextStack.lastIndexOf(ctx);
|
|
161
|
+
if (idx !== -1) {
|
|
162
|
+
contextStack.splice(idx, 1);
|
|
163
|
+
}
|
|
164
|
+
activeContext = contextStack.length > 0 ? contextStack[contextStack.length - 1] : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function onMount(fn) {
|
|
168
|
+
if (activeContext) {
|
|
169
|
+
activeContext.mounts.push(fn);
|
|
170
|
+
} else {
|
|
171
|
+
fn();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function onDestroy(fn) {
|
|
176
|
+
if (activeContext) {
|
|
177
|
+
activeContext.destroys.push(fn);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Called by compiled mount() function to flush instance mounts
|
|
182
|
+
export function __flush_mounts() {
|
|
183
|
+
if (activeContext && activeContext.mounts.length > 0) {
|
|
184
|
+
const cbs = [...activeContext.mounts];
|
|
185
|
+
activeContext.mounts = [];
|
|
186
|
+
for (const cb of cbs) {
|
|
187
|
+
cb();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Called when a component is torn down
|
|
193
|
+
export function __flush_destroys() {
|
|
194
|
+
if (activeContext && activeContext.destroys.length > 0) {
|
|
195
|
+
const cbs = [...activeContext.destroys];
|
|
196
|
+
activeContext.destroys = [];
|
|
197
|
+
for (const cb of cbs) {
|
|
198
|
+
cb();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── createIntent ───
|
|
204
|
+
// Configurable ambient intent resolver with streaming support.
|
|
205
|
+
const defaultIntentConfig = {
|
|
206
|
+
provider: 'local',
|
|
207
|
+
endpoint: '/api/intent',
|
|
208
|
+
model: 'gemini-2.5-flash',
|
|
209
|
+
stream: false
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let globalIntentConfig = { ...defaultIntentConfig };
|
|
213
|
+
|
|
214
|
+
export function configureIntent(config) {
|
|
215
|
+
globalIntentConfig = { ...globalIntentConfig, ...config };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function _consumeSSE(response, onToken, onDone, onError) {
|
|
219
|
+
const reader = response.body.getReader();
|
|
220
|
+
const decoder = new TextDecoder();
|
|
221
|
+
let buf = '';
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
while (true) {
|
|
225
|
+
const { done, value } = await reader.read();
|
|
226
|
+
if (done) break;
|
|
227
|
+
buf += decoder.decode(value, { stream: true });
|
|
228
|
+
|
|
229
|
+
const lines = buf.split('\n');
|
|
230
|
+
buf = lines.pop(); // keep incomplete last line
|
|
231
|
+
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
if (!line.startsWith('data: ')) continue;
|
|
234
|
+
const payload = line.slice(6).trim();
|
|
235
|
+
if (payload === '[DONE]') { onDone(); return; }
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(payload);
|
|
238
|
+
const token = parsed.token ?? parsed.delta ?? parsed.content ?? '';
|
|
239
|
+
if (token) onToken(token);
|
|
240
|
+
} catch {
|
|
241
|
+
if (payload) onToken(payload);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
onDone();
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (err.name !== 'AbortError') onError(err);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function createIntent(promptFn, options = {}) {
|
|
252
|
+
const config = { ...globalIntentConfig, ...options };
|
|
253
|
+
const [read, write] = createSignal(options.initial ?? null);
|
|
254
|
+
const [loading, setLoading] = createSignal(false);
|
|
255
|
+
const [error, setError] = createSignal(null);
|
|
256
|
+
let abortController = null;
|
|
257
|
+
|
|
258
|
+
onDestroy(() => {
|
|
259
|
+
if (abortController) abortController.abort();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
createEffect(() => {
|
|
263
|
+
const prompt = typeof promptFn === 'function' ? promptFn() : promptFn;
|
|
264
|
+
if (!prompt) return;
|
|
265
|
+
|
|
266
|
+
if (abortController) abortController.abort();
|
|
267
|
+
abortController = new AbortController();
|
|
268
|
+
|
|
269
|
+
write(null);
|
|
270
|
+
setError(null);
|
|
271
|
+
setLoading(true);
|
|
272
|
+
|
|
273
|
+
const body = JSON.stringify({
|
|
274
|
+
messages: [{ role: 'user', content: prompt }],
|
|
275
|
+
model: config.model,
|
|
276
|
+
provider: config.provider,
|
|
277
|
+
stream: config.stream
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
fetch(config.endpoint, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body,
|
|
284
|
+
signal: abortController.signal
|
|
285
|
+
}).then(res => {
|
|
286
|
+
if (!res.ok) throw new Error(`Intent failed: ${res.status}`);
|
|
287
|
+
|
|
288
|
+
if (config.stream) {
|
|
289
|
+
let accumulated = '';
|
|
290
|
+
return _consumeSSE(
|
|
291
|
+
res,
|
|
292
|
+
token => { accumulated += token; write(accumulated); },
|
|
293
|
+
() => setLoading(false),
|
|
294
|
+
err => { setError(err.message); setLoading(false); }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return res.json().then(data => {
|
|
299
|
+
if (data?.components?.length > 0) write(data.components[0]);
|
|
300
|
+
else if (data?.result != null) write(data.result);
|
|
301
|
+
else write(data);
|
|
302
|
+
setLoading(false);
|
|
303
|
+
});
|
|
304
|
+
}).catch(err => {
|
|
305
|
+
if (err.name !== 'AbortError') {
|
|
306
|
+
console.error('[Sola Intent Error]', err);
|
|
307
|
+
setError(err.message);
|
|
308
|
+
setLoading(false);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const accessor = read;
|
|
314
|
+
accessor.loading = loading;
|
|
315
|
+
accessor.error = error;
|
|
316
|
+
return accessor;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── createData ───
|
|
320
|
+
// Reactive data connection through the Sola Relay.
|
|
321
|
+
const defaultDataConfig = {
|
|
322
|
+
relayEndpoint: 'http://localhost:4040/api/query',
|
|
323
|
+
refresh: null // e.g. '30s', '1m', '5m'
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
let globalDataConfig = { ...defaultDataConfig };
|
|
327
|
+
|
|
328
|
+
export function configureData(config) {
|
|
329
|
+
globalDataConfig = { ...globalDataConfig, ...config };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseInterval(str) {
|
|
333
|
+
if (!str) return null;
|
|
334
|
+
const match = str.match(/^(\d+)(s|m|h)$/);
|
|
335
|
+
if (!match) return null;
|
|
336
|
+
const val = parseInt(match[1]);
|
|
337
|
+
switch (match[2]) {
|
|
338
|
+
case 's': return val * 1000;
|
|
339
|
+
case 'm': return val * 60 * 1000;
|
|
340
|
+
case 'h': return val * 3600 * 1000;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function createData(source, options = {}) {
|
|
346
|
+
const config = { ...globalDataConfig, ...options };
|
|
347
|
+
const [read, write] = createSignal({ loading: true, data: null, error: null });
|
|
348
|
+
let abortController = null;
|
|
349
|
+
let refreshTimer = null;
|
|
350
|
+
|
|
351
|
+
function fetchData() {
|
|
352
|
+
if (abortController) abortController.abort();
|
|
353
|
+
abortController = new AbortController();
|
|
354
|
+
|
|
355
|
+
write({ loading: true, data: read().data, error: null });
|
|
356
|
+
|
|
357
|
+
fetch(config.relayEndpoint, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: { 'Content-Type': 'application/json' },
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
source,
|
|
362
|
+
query: config.query || null,
|
|
363
|
+
filters: config.filters || null,
|
|
364
|
+
sort: config.sort || null,
|
|
365
|
+
limit: config.limit || null,
|
|
366
|
+
offset: config.offset || null
|
|
367
|
+
}),
|
|
368
|
+
signal: abortController.signal
|
|
369
|
+
})
|
|
370
|
+
.then(res => {
|
|
371
|
+
if (!res.ok) throw new Error(`Data fetch failed: ${res.status}`);
|
|
372
|
+
return res.json();
|
|
373
|
+
})
|
|
374
|
+
.then(data => {
|
|
375
|
+
write({ loading: false, data: data.rows || data, error: null });
|
|
376
|
+
})
|
|
377
|
+
.catch(err => {
|
|
378
|
+
if (err.name !== 'AbortError') {
|
|
379
|
+
console.error('[Sola Data Error]', err);
|
|
380
|
+
write({ loading: false, data: null, error: err.message });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Initial fetch
|
|
386
|
+
fetchData();
|
|
387
|
+
|
|
388
|
+
// Auto-refresh
|
|
389
|
+
const interval = parseInterval(config.refresh);
|
|
390
|
+
if (interval) {
|
|
391
|
+
refreshTimer = setInterval(fetchData, interval);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Return a signal that provides .loading, .data, .error
|
|
395
|
+
const accessor = () => read();
|
|
396
|
+
accessor.refetch = fetchData;
|
|
397
|
+
accessor.stop = () => {
|
|
398
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
399
|
+
if (abortController) abortController.abort();
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
return accessor;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ─── Named Cross-Widget Signal Telemetry Mesh ───
|
|
406
|
+
|
|
407
|
+
class SignalMeshEngine {
|
|
408
|
+
constructor() {
|
|
409
|
+
this.topics = new Map();
|
|
410
|
+
this.telemetrySubscribers = new Set();
|
|
411
|
+
this.cycleStack = new Set();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
topic(name, initialValue) {
|
|
415
|
+
if (!this.topics.has(name)) {
|
|
416
|
+
const [read, write] = createSignal(initialValue);
|
|
417
|
+
this.topics.set(name, { read, write, value: initialValue, subscribers: new Set() });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const entry = this.topics.get(name);
|
|
421
|
+
|
|
422
|
+
const read = () => entry.read();
|
|
423
|
+
const write = (next, originId = 'signal') => {
|
|
424
|
+
const nextVal = typeof next === 'function' ? next(entry.value) : next;
|
|
425
|
+
if (entry.value === nextVal) return;
|
|
426
|
+
|
|
427
|
+
if (this.cycleStack.has(name)) {
|
|
428
|
+
console.warn(`[Sola Signal Mesh] Cycle detected on topic "${name}". Aborting cyclic dispatch.`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const prev = entry.value;
|
|
433
|
+
entry.value = nextVal;
|
|
434
|
+
entry.write(nextVal);
|
|
435
|
+
|
|
436
|
+
const event = {
|
|
437
|
+
topic: name,
|
|
438
|
+
value: nextVal,
|
|
439
|
+
prevValue: prev,
|
|
440
|
+
timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),
|
|
441
|
+
originWidgetId: originId
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
this.telemetrySubscribers.forEach(cb => {
|
|
445
|
+
try { cb(event); } catch (e) { console.error(e); }
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
this.cycleStack.add(name);
|
|
449
|
+
try {
|
|
450
|
+
entry.subscribers.forEach(sub => {
|
|
451
|
+
try { sub(nextVal, event); } catch (e) { console.error(e); }
|
|
452
|
+
});
|
|
453
|
+
} finally {
|
|
454
|
+
this.cycleStack.delete(name);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
return [read, write];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
subscribe(name, fn) {
|
|
462
|
+
if (!this.topics.has(name)) {
|
|
463
|
+
this.topic(name, undefined);
|
|
464
|
+
}
|
|
465
|
+
const entry = this.topics.get(name);
|
|
466
|
+
entry.subscribers.add(fn);
|
|
467
|
+
return () => entry.subscribers.delete(fn);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
onTelemetry(fn) {
|
|
471
|
+
this.telemetrySubscribers.add(fn);
|
|
472
|
+
return () => this.telemetrySubscribers.delete(fn);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export const signalMesh = new SignalMeshEngine();
|
|
477
|
+
export const createTopicSignal = (topic, initialVal) => signalMesh.topic(topic, initialVal);
|
|
478
|
+
|
|
479
|
+
// ─── Sola Sentinel & Intent Telemetry Observer ───
|
|
480
|
+
export class SolaSentinel {
|
|
481
|
+
constructor(name = 'default', options = {}) {
|
|
482
|
+
this.name = name;
|
|
483
|
+
this.thresholdMs = options.thresholdMs || 600;
|
|
484
|
+
this.maxRageClicks = options.maxRageClicks || 3;
|
|
485
|
+
this.clickHistory = [];
|
|
486
|
+
this.subscribers = new Set();
|
|
487
|
+
this.frictionEvents = [];
|
|
488
|
+
this.flowIndex = 99.8;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
recordClick(actionId, target = 'button') {
|
|
492
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
493
|
+
this.clickHistory.push({ actionId, target, timestamp: now });
|
|
494
|
+
this.clickHistory = this.clickHistory.filter(c => now - c.timestamp < 2000);
|
|
495
|
+
|
|
496
|
+
const recent = this.clickHistory.filter(c => c.actionId === actionId && now - c.timestamp < this.thresholdMs);
|
|
497
|
+
if (recent.length >= this.maxRageClicks) {
|
|
498
|
+
this.triggerFrictionAlert({
|
|
499
|
+
type: 'RAGE_CLICK',
|
|
500
|
+
actionId,
|
|
501
|
+
target,
|
|
502
|
+
count: recent.length,
|
|
503
|
+
timestamp: now,
|
|
504
|
+
severity: 'HIGH',
|
|
505
|
+
message: `Rage-click burst: ${recent.length} taps in ${Math.round(now - recent[0].timestamp)}ms`
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
recordSignalDrop(topic, error) {
|
|
511
|
+
this.triggerFrictionAlert({
|
|
512
|
+
type: 'SIGNAL_TIMEOUT',
|
|
513
|
+
topic,
|
|
514
|
+
error: error?.message || String(error),
|
|
515
|
+
timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),
|
|
516
|
+
severity: 'CRITICAL',
|
|
517
|
+
message: `Signal channel "${topic}" breached SLA timeout (504 Gateway Stall)`
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
triggerFrictionAlert(event) {
|
|
522
|
+
this.frictionEvents.unshift(event);
|
|
523
|
+
if (this.frictionEvents.length > 50) this.frictionEvents.pop();
|
|
524
|
+
this.flowIndex = Math.max(68.5, Number((this.flowIndex - 3.8).toFixed(1)));
|
|
525
|
+
|
|
526
|
+
this.subscribers.forEach(cb => {
|
|
527
|
+
try { cb(event, this); } catch(e) { console.error(e); }
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
onFriction(cb) {
|
|
532
|
+
this.subscribers.add(cb);
|
|
533
|
+
return () => this.subscribers.delete(cb);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function createSentinel(name, options) {
|
|
538
|
+
return new SolaSentinel(name, options);
|
|
539
|
+
}
|
|
540
|
+
|