@sola-air-ui/core 1.0.0 → 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.
Files changed (3) hide show
  1. package/build.js +33 -0
  2. package/package.json +15 -4
  3. 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,18 +1,29 @@
1
1
  {
2
2
  "name": "@sola-air-ui/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Zero-VDOM reactivity engine — signals, effects, lifecycle, and intent primitives",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "exports": {
8
- ".": "./src/index.js"
8
+ ".": "./src/index.js",
9
+ "./iife": "./dist/sola-core.iife.js"
9
10
  },
10
- "keywords": ["sola", "reactivity", "signals", "zero-vdom", "ui", "frontend"],
11
+ "scripts": {
12
+ "build": "node build.js"
13
+ },
14
+ "keywords": [
15
+ "sola",
16
+ "reactivity",
17
+ "signals",
18
+ "zero-vdom",
19
+ "ui",
20
+ "frontend"
21
+ ],
11
22
  "license": "MIT",
12
23
  "homepage": "https://sola-air.dev",
13
24
  "repository": {
14
25
  "type": "git",
15
- "url": "https://github.com/rbm3267/sola"
26
+ "url": "https://github.com/rbm3267/sola-air"
16
27
  },
17
28
  "publishConfig": {
18
29
  "access": "public",
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
- let globalIntentConfig = { ...defaultIntentConfig };
212
-
213
- export function configureIntent(config) {
214
- globalIntentConfig = { ...globalIntentConfig, ...config };
215
- }
216
-
217
- export function createIntent(promptFn, options = {}) {
218
- const config = { ...globalIntentConfig, ...options };
219
- const [read, write] = createSignal(options.initial || 'Resolving...');
220
- let abortController = null;
221
-
222
- onDestroy(() => {
223
- if (abortController) abortController.abort();
224
- });
225
-
226
- createEffect(() => {
227
- const currentPrompt = typeof promptFn === 'function' ? promptFn() : promptFn;
228
- if (!currentPrompt) return;
229
-
230
- if (abortController) {
231
- abortController.abort();
232
- }
233
- abortController = new AbortController();
234
-
235
- const url = config.endpoint;
236
- write('Resolving...');
237
-
238
- fetch(url, {
239
- method: 'POST',
240
- headers: { 'Content-Type': 'application/json' },
241
- body: JSON.stringify({
242
- messages: [{ role: 'user', content: currentPrompt }],
243
- model: config.model,
244
- provider: config.provider
245
- }),
246
- signal: abortController.signal
247
- })
248
- .then(res => {
249
- if (!res.ok) throw new Error(`Intent failed: ${res.status}`);
250
- return res.json();
251
- })
252
- .then(data => {
253
- if (data && data.components && data.components.length > 0) {
254
- write(data.components[0]);
255
- } else if (data && data.result) {
256
- write(data.result);
257
- } else {
258
- write(data);
259
- }
260
- })
261
- .catch(err => {
262
- if (err.name !== 'AbortError') {
263
- console.error('[Sola Intent Error]', err);
264
- write({ error: err.message });
265
- }
266
- });
267
- });
268
-
269
- return read;
270
- }
271
-
272
- // ─── createData ───
273
- // Reactive data connection through the Sola Relay.
274
- const defaultDataConfig = {
275
- relayEndpoint: 'http://localhost:4040/api/query',
276
- refresh: null // e.g. '30s', '1m', '5m'
277
- };
278
-
279
- let globalDataConfig = { ...defaultDataConfig };
280
-
281
- export function configureData(config) {
282
- globalDataConfig = { ...globalDataConfig, ...config };
283
- }
284
-
285
- function parseInterval(str) {
286
- if (!str) return null;
287
- const match = str.match(/^(\d+)(s|m|h)$/);
288
- if (!match) return null;
289
- const val = parseInt(match[1]);
290
- switch (match[2]) {
291
- case 's': return val * 1000;
292
- case 'm': return val * 60 * 1000;
293
- case 'h': return val * 3600 * 1000;
294
- }
295
- return null;
296
- }
297
-
298
- export function createData(source, options = {}) {
299
- const config = { ...globalDataConfig, ...options };
300
- const [read, write] = createSignal({ loading: true, data: null, error: null });
301
- let abortController = null;
302
- let refreshTimer = null;
303
-
304
- function fetchData() {
305
- if (abortController) abortController.abort();
306
- abortController = new AbortController();
307
-
308
- write({ loading: true, data: read().data, error: null });
309
-
310
- fetch(config.relayEndpoint, {
311
- method: 'POST',
312
- headers: { 'Content-Type': 'application/json' },
313
- body: JSON.stringify({
314
- source,
315
- query: config.query || null,
316
- filters: config.filters || null,
317
- sort: config.sort || null,
318
- limit: config.limit || null,
319
- offset: config.offset || null
320
- }),
321
- signal: abortController.signal
322
- })
323
- .then(res => {
324
- if (!res.ok) throw new Error(`Data fetch failed: ${res.status}`);
325
- return res.json();
326
- })
327
- .then(data => {
328
- write({ loading: false, data: data.rows || data, error: null });
329
- })
330
- .catch(err => {
331
- if (err.name !== 'AbortError') {
332
- console.error('[Sola Data Error]', err);
333
- write({ loading: false, data: null, error: err.message });
334
- }
335
- });
336
- }
337
-
338
- // Initial fetch
339
- fetchData();
340
-
341
- // Auto-refresh
342
- const interval = parseInterval(config.refresh);
343
- if (interval) {
344
- refreshTimer = setInterval(fetchData, interval);
345
- }
346
-
347
- // Return a signal that provides .loading, .data, .error
348
- const accessor = () => read();
349
- accessor.refetch = fetchData;
350
- accessor.stop = () => {
351
- if (refreshTimer) clearInterval(refreshTimer);
352
- if (abortController) abortController.abort();
353
- };
354
-
355
- return accessor;
356
- }
357
-
358
- // ─── Named Cross-Widget Signal Telemetry Mesh ───
359
-
360
- class SignalMeshEngine {
361
- constructor() {
362
- this.topics = new Map();
363
- this.telemetrySubscribers = new Set();
364
- this.cycleStack = new Set();
365
- }
366
-
367
- topic(name, initialValue) {
368
- if (!this.topics.has(name)) {
369
- const [read, write] = createSignal(initialValue);
370
- this.topics.set(name, { read, write, value: initialValue, subscribers: new Set() });
371
- }
372
-
373
- const entry = this.topics.get(name);
374
-
375
- const read = () => entry.read();
376
- const write = (next, originId = 'signal') => {
377
- const nextVal = typeof next === 'function' ? next(entry.value) : next;
378
- if (entry.value === nextVal) return;
379
-
380
- if (this.cycleStack.has(name)) {
381
- console.warn(`[Sola Signal Mesh] Cycle detected on topic "${name}". Aborting cyclic dispatch.`);
382
- return;
383
- }
384
-
385
- const prev = entry.value;
386
- entry.value = nextVal;
387
- entry.write(nextVal);
388
-
389
- const event = {
390
- topic: name,
391
- value: nextVal,
392
- prevValue: prev,
393
- timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),
394
- originWidgetId: originId
395
- };
396
-
397
- this.telemetrySubscribers.forEach(cb => {
398
- try { cb(event); } catch (e) { console.error(e); }
399
- });
400
-
401
- this.cycleStack.add(name);
402
- try {
403
- entry.subscribers.forEach(sub => {
404
- try { sub(nextVal, event); } catch (e) { console.error(e); }
405
- });
406
- } finally {
407
- this.cycleStack.delete(name);
408
- }
409
- };
410
-
411
- return [read, write];
412
- }
413
-
414
- subscribe(name, fn) {
415
- if (!this.topics.has(name)) {
416
- this.topic(name, undefined);
417
- }
418
- const entry = this.topics.get(name);
419
- entry.subscribers.add(fn);
420
- return () => entry.subscribers.delete(fn);
421
- }
422
-
423
- onTelemetry(fn) {
424
- this.telemetrySubscribers.add(fn);
425
- return () => this.telemetrySubscribers.delete(fn);
426
- }
427
- }
428
-
429
- export const signalMesh = new SignalMeshEngine();
430
- export const createTopicSignal = (topic, initialVal) => signalMesh.topic(topic, initialVal);
431
-
432
- // ─── Sola Sentinel & Intent Telemetry Observer ───
433
- export class SolaSentinel {
434
- constructor(name = 'default', options = {}) {
435
- this.name = name;
436
- this.thresholdMs = options.thresholdMs || 600;
437
- this.maxRageClicks = options.maxRageClicks || 3;
438
- this.clickHistory = [];
439
- this.subscribers = new Set();
440
- this.frictionEvents = [];
441
- this.flowIndex = 99.8;
442
- }
443
-
444
- recordClick(actionId, target = 'button') {
445
- const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
446
- this.clickHistory.push({ actionId, target, timestamp: now });
447
- this.clickHistory = this.clickHistory.filter(c => now - c.timestamp < 2000);
448
-
449
- const recent = this.clickHistory.filter(c => c.actionId === actionId && now - c.timestamp < this.thresholdMs);
450
- if (recent.length >= this.maxRageClicks) {
451
- this.triggerFrictionAlert({
452
- type: 'RAGE_CLICK',
453
- actionId,
454
- target,
455
- count: recent.length,
456
- timestamp: now,
457
- severity: 'HIGH',
458
- message: `Rage-click burst: ${recent.length} taps in ${Math.round(now - recent[0].timestamp)}ms`
459
- });
460
- }
461
- }
462
-
463
- recordSignalDrop(topic, error) {
464
- this.triggerFrictionAlert({
465
- type: 'SIGNAL_TIMEOUT',
466
- topic,
467
- error: error?.message || String(error),
468
- timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),
469
- severity: 'CRITICAL',
470
- message: `Signal channel "${topic}" breached SLA timeout (504 Gateway Stall)`
471
- });
472
- }
473
-
474
- triggerFrictionAlert(event) {
475
- this.frictionEvents.unshift(event);
476
- if (this.frictionEvents.length > 50) this.frictionEvents.pop();
477
- this.flowIndex = Math.max(68.5, Number((this.flowIndex - 3.8).toFixed(1)));
478
-
479
- this.subscribers.forEach(cb => {
480
- try { cb(event, this); } catch(e) { console.error(e); }
481
- });
482
- }
483
-
484
- onFriction(cb) {
485
- this.subscribers.add(cb);
486
- return () => this.subscribers.delete(cb);
487
- }
488
- }
489
-
490
- export function createSentinel(name, options) {
491
- return new SolaSentinel(name, options);
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
+