@africode/core 5.0.0 → 5.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/COMPONENT_SCHEMA.json +837 -0
- package/components/base.d.ts +1 -1
- package/components/base.js +71 -21
- package/core/a2ui-schema-manager.js +9 -2
- package/core/a2ui.js +131 -43
- package/core/actions.js +27 -0
- package/core/bun-runtime.js +207 -724
- package/core/compliance.js +6 -5
- package/core/config.js +7 -5
- package/core/enhanced-hmr.js +16 -14
- package/core/file-router.js +42 -282
- package/core/hmr.js +8 -7
- package/core/html.d.ts +15 -101
- package/core/html.js +53 -129
- package/core/lipa-namba-journey.js +72 -61
- package/core/logging.js +14 -0
- package/core/middleware.js +82 -0
- package/core/nida-cig-middleware.js +13 -8
- package/core/plugins/index.js +345 -312
- package/core/request-identity.js +44 -0
- package/core/sdk.js +22 -0
- package/core/session-store.js +68 -0
- package/core/state.js +34 -0
- package/core/websocket.js +22 -20
- package/dist/africode.js +108 -112
- package/dist/africode.js.map +6 -6
- package/dist/build-info.json +3 -3
- package/dist/components.js +351 -351
- package/dist/components.js.map +6 -6
- package/package.json +6 -4
- package/scripts/generate-component-schema.js +80 -0
package/components/base.d.ts
CHANGED
package/components/base.js
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { html } from '../core/html.js';
|
|
11
11
|
|
|
12
|
+
const BaseHTMLElement = globalThis.HTMLElement ?? class {};
|
|
13
|
+
const hasConstructableStylesheets = typeof CSSStyleSheet !== 'undefined';
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Base class for AfriCode components
|
|
14
17
|
* All components extend this to inherit common functionality
|
|
15
18
|
*/
|
|
16
|
-
export class AfriCodeComponent extends
|
|
19
|
+
export class AfriCodeComponent extends BaseHTMLElement {
|
|
17
20
|
constructor() {
|
|
18
21
|
super();
|
|
19
22
|
this.attachShadow({ mode: 'open' });
|
|
@@ -84,8 +87,11 @@ export class AfriCodeComponent extends HTMLElement {
|
|
|
84
87
|
* @param {CSSStyleSheet} sheet - A Constructable Stylesheet instance
|
|
85
88
|
*/
|
|
86
89
|
static injectGlobalSheet(sheet) {
|
|
90
|
+
if (!hasConstructableStylesheets) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
if (!(sheet instanceof CSSStyleSheet)) {
|
|
88
|
-
console.error('[AfriCode] injectGlobalSheet requires a CSSStyleSheet instance.');
|
|
89
95
|
return;
|
|
90
96
|
}
|
|
91
97
|
if (!AfriCodeComponent.globalSheets.includes(sheet)) {
|
|
@@ -98,37 +104,77 @@ export class AfriCodeComponent extends HTMLElement {
|
|
|
98
104
|
* Optimized to fetch only once per session.
|
|
99
105
|
* Also adopts any globally injected sheets (e.g. Tailwind utilities).
|
|
100
106
|
*/
|
|
101
|
-
async loadStyles() {
|
|
107
|
+
async loadStyles(css = '') {
|
|
102
108
|
const styleUrl = '/styles/africanity.css';
|
|
109
|
+
if (!this.shadowRoot) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const canAdoptStyleSheets = hasConstructableStylesheets && 'adoptedStyleSheets' in this.shadowRoot;
|
|
114
|
+
const sourceKey = typeof css === 'string' && css.length > 0 ? css : styleUrl;
|
|
115
|
+
const stylesheetText = typeof css === 'string' && css.length > 0 ? css : null;
|
|
103
116
|
|
|
104
117
|
// 1. Check cache first
|
|
105
|
-
if (AfriCodeComponent.styleCache.has(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
if (AfriCodeComponent.styleCache.has(sourceKey)) {
|
|
119
|
+
const cached = AfriCodeComponent.styleCache.get(sourceKey);
|
|
120
|
+
if (canAdoptStyleSheets) {
|
|
121
|
+
this.shadowRoot.adoptedStyleSheets = [
|
|
122
|
+
cached,
|
|
123
|
+
...AfriCodeComponent.globalSheets
|
|
124
|
+
];
|
|
125
|
+
} else {
|
|
126
|
+
this._applyFallbackStyle(cached?.cssText || cached?.text || cached || '');
|
|
127
|
+
}
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
// 2. Fetch if not cached (Single Request)
|
|
114
132
|
try {
|
|
115
|
-
|
|
116
|
-
const css = await response.text();
|
|
133
|
+
let resolvedCss = stylesheetText;
|
|
117
134
|
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
if (!resolvedCss) {
|
|
136
|
+
const response = await fetch(styleUrl);
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`Unable to load stylesheet: ${response.status}`);
|
|
139
|
+
}
|
|
140
|
+
resolvedCss = await response.text();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (canAdoptStyleSheets) {
|
|
144
|
+
const styleSheet = new CSSStyleSheet();
|
|
145
|
+
styleSheet.replaceSync(resolvedCss);
|
|
146
|
+
|
|
147
|
+
// Cache it
|
|
148
|
+
AfriCodeComponent.styleCache.set(sourceKey, styleSheet);
|
|
149
|
+
|
|
150
|
+
// Apply core + any global sheets
|
|
151
|
+
this.shadowRoot.adoptedStyleSheets = [
|
|
152
|
+
styleSheet,
|
|
153
|
+
...AfriCodeComponent.globalSheets
|
|
154
|
+
];
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._applyFallbackStyle(resolvedCss);
|
|
159
|
+
AfriCodeComponent.styleCache.set(sourceKey, { text: resolvedCss });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
this._applyFallbackStyle(stylesheetText || '');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
120
164
|
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
_applyFallbackStyle(css) {
|
|
166
|
+
if (!this.shadowRoot) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
123
169
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} catch (err) {
|
|
130
|
-
console.warn('AfriCode: Failed to load core styles', err);
|
|
170
|
+
let style = this.shadowRoot.querySelector('style[data-africode-core-styles]');
|
|
171
|
+
if (!style) {
|
|
172
|
+
style = document.createElement('style');
|
|
173
|
+
style.setAttribute('data-africode-core-styles', '');
|
|
174
|
+
this.shadowRoot.prepend(style);
|
|
131
175
|
}
|
|
176
|
+
|
|
177
|
+
style.textContent = css;
|
|
132
178
|
}
|
|
133
179
|
|
|
134
180
|
/**
|
|
@@ -175,6 +221,10 @@ export class AfriCodeComponent extends HTMLElement {
|
|
|
175
221
|
* @param {typeof HTMLElement} component - Component class
|
|
176
222
|
*/
|
|
177
223
|
export function registerComponent(name, component) {
|
|
224
|
+
if (typeof customElements === 'undefined') {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
178
228
|
if (!customElements.get(name)) {
|
|
179
229
|
customElements.define(name, component);
|
|
180
230
|
}
|
|
@@ -170,7 +170,7 @@ export class A2UISchemaManager {
|
|
|
170
170
|
}));
|
|
171
171
|
|
|
172
172
|
// Extract CSS custom properties
|
|
173
|
-
const cssMatches = [...content.matchAll(/var\(
|
|
173
|
+
const cssMatches = [...content.matchAll(/var\(--([a-z0-9-]+)\)/g)];
|
|
174
174
|
const cssCustomProperties = cssMatches.map(m => '--' + m[1]);
|
|
175
175
|
|
|
176
176
|
// Extract JSDoc comments
|
|
@@ -217,7 +217,7 @@ export class A2UISchemaManager {
|
|
|
217
217
|
errors.push('Forbidden: eval() detected');
|
|
218
218
|
}
|
|
219
219
|
} else if (pattern === 'innerHTML = userInput') {
|
|
220
|
-
if (/innerHTML\s
|
|
220
|
+
if (/innerHTML\s*=/i.test(html)) {
|
|
221
221
|
warnings.push('Warning: innerHTML assignment detected — ensure content is sanitized');
|
|
222
222
|
}
|
|
223
223
|
}
|
|
@@ -266,6 +266,11 @@ export class A2UISchemaManager {
|
|
|
266
266
|
|
|
267
267
|
return this.schema;
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validate component props against the generated schema
|
|
272
|
+
*/
|
|
273
|
+
validateComponent(tagName, props = {}) {
|
|
269
274
|
const component = this.schema.components.find(c => c.tagName === tagName);
|
|
270
275
|
if (!component) {
|
|
271
276
|
return { valid: false, error: `Component ${tagName} not found in schema` };
|
|
@@ -327,6 +332,8 @@ export class A2UISchemaManager {
|
|
|
327
332
|
}
|
|
328
333
|
}
|
|
329
334
|
|
|
335
|
+
export { A2UISchemaManager as A2uiSchemaManager };
|
|
336
|
+
|
|
330
337
|
/**
|
|
331
338
|
* CLI Usage
|
|
332
339
|
* node core/a2ui-schema-manager.js generate
|
package/core/a2ui.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @module core/a2ui
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { componentMap as coreComponentMap } from '../components/index.js';
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* A2UI Message Types
|
|
13
15
|
*/
|
|
@@ -37,6 +39,51 @@ export const A2UI_SCHEMAS = {
|
|
|
37
39
|
}
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
function escapeHtml(value) {
|
|
43
|
+
return String(value)
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/</g, '<')
|
|
46
|
+
.replace(/>/g, '>')
|
|
47
|
+
.replace(/"/g, '"')
|
|
48
|
+
.replace(/'/g, ''');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderNode(node) {
|
|
52
|
+
if (Array.isArray(node)) {
|
|
53
|
+
return node.map(renderNode).join('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (node === null || node === undefined || node === false) {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') {
|
|
61
|
+
return escapeHtml(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof node !== 'object') {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tag = node.tag || node.tagName;
|
|
69
|
+
if (!tag) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = node.props || {};
|
|
74
|
+
const attributes = Object.entries(props)
|
|
75
|
+
.filter(([, value]) => value !== null && value !== undefined && value !== false)
|
|
76
|
+
.map(([key, value]) => value === true ? key : `${key}="${escapeHtml(value)}"`)
|
|
77
|
+
.join(' ');
|
|
78
|
+
|
|
79
|
+
const children = renderNode(node.children ?? node.content ?? '');
|
|
80
|
+
return `<${tag}${attributes ? ` ${attributes}` : ''}>${children}</${tag}>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isUnsafeHtml(html) {
|
|
84
|
+
return /<\s*script\b|on\w+\s*=|javascript:/i.test(String(html));
|
|
85
|
+
}
|
|
86
|
+
|
|
40
87
|
/**
|
|
41
88
|
* A2UI Renderer
|
|
42
89
|
* Renders A2UI messages into actual DOM elements
|
|
@@ -51,63 +98,74 @@ export class A2UIRenderer {
|
|
|
51
98
|
* Render an A2UI message
|
|
52
99
|
* @param {Object} message - A2UI message object
|
|
53
100
|
*/
|
|
54
|
-
|
|
101
|
+
render(message) {
|
|
55
102
|
if (!message || !message.type) {
|
|
56
103
|
throw new Error('Invalid A2UI message');
|
|
57
104
|
}
|
|
58
105
|
|
|
59
|
-
|
|
106
|
+
if (message.html !== undefined) {
|
|
107
|
+
if (isUnsafeHtml(message.html)) {
|
|
108
|
+
throw new Error('Unsafe HTML is not allowed in A2UI render messages');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error('Raw HTML payloads are not supported in A2UI render messages');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const output = Array.isArray(message.components)
|
|
115
|
+
? message.components.map(renderNode).join('')
|
|
116
|
+
: renderNode(message.component || message);
|
|
60
117
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
id: message.id
|
|
118
|
+
if (message.id) {
|
|
119
|
+
this.components.set(message.id, {
|
|
120
|
+
id: message.id,
|
|
64
121
|
type: message.type,
|
|
65
122
|
props: message.props || {},
|
|
66
|
-
children: message.children || []
|
|
67
|
-
};
|
|
123
|
+
children: message.children || message.components || []
|
|
124
|
+
});
|
|
125
|
+
}
|
|
68
126
|
|
|
69
|
-
|
|
70
|
-
|
|
127
|
+
const canRenderToDOM = this.root && typeof this.root.appendChild === 'function' && typeof document !== 'undefined';
|
|
128
|
+
if (canRenderToDOM && output) {
|
|
129
|
+
const container = document.createElement('div');
|
|
130
|
+
container.innerHTML = output;
|
|
131
|
+
while (container.firstChild) {
|
|
132
|
+
this.root.appendChild(container.firstChild);
|
|
71
133
|
}
|
|
72
|
-
|
|
73
|
-
return placeholder;
|
|
74
134
|
}
|
|
75
135
|
|
|
76
|
-
|
|
77
|
-
this.root.appendChild(element);
|
|
78
|
-
return element;
|
|
136
|
+
return output;
|
|
79
137
|
}
|
|
80
138
|
|
|
81
139
|
/**
|
|
82
140
|
* Create DOM element from A2UI component
|
|
83
141
|
* @param {Object} component - A2UI component definition
|
|
84
142
|
*/
|
|
85
|
-
|
|
143
|
+
createElement(component) {
|
|
86
144
|
const { type, props = {}, children = [] } = component;
|
|
87
145
|
|
|
88
146
|
let element;
|
|
89
147
|
|
|
90
148
|
switch (type) {
|
|
91
149
|
case A2UI_TYPES.COMPONENT:
|
|
92
|
-
element =
|
|
150
|
+
element = this.createCustomComponent(props);
|
|
93
151
|
break;
|
|
94
152
|
case A2UI_TYPES.FORM:
|
|
95
|
-
element =
|
|
153
|
+
element = this.createForm(props);
|
|
96
154
|
break;
|
|
97
155
|
case A2UI_TYPES.LIST:
|
|
98
|
-
element =
|
|
156
|
+
element = this.createList(props);
|
|
99
157
|
break;
|
|
100
158
|
case A2UI_TYPES.CARD:
|
|
101
|
-
element =
|
|
159
|
+
element = this.createCard(props);
|
|
102
160
|
break;
|
|
103
161
|
case A2UI_TYPES.MODAL:
|
|
104
|
-
element =
|
|
162
|
+
element = this.createModal(props);
|
|
105
163
|
break;
|
|
106
164
|
case A2UI_TYPES.TOAST:
|
|
107
|
-
element =
|
|
165
|
+
element = this.createToast(props);
|
|
108
166
|
break;
|
|
109
167
|
case A2UI_TYPES.PROGRESS:
|
|
110
|
-
element =
|
|
168
|
+
element = this.createProgress(props);
|
|
111
169
|
break;
|
|
112
170
|
default:
|
|
113
171
|
throw new Error(`Unknown A2UI component type: ${type}`);
|
|
@@ -116,7 +174,7 @@ export class A2UIRenderer {
|
|
|
116
174
|
// Render children
|
|
117
175
|
if (children.length > 0) {
|
|
118
176
|
for (const child of children) {
|
|
119
|
-
const childElement =
|
|
177
|
+
const childElement = this.createElement(child);
|
|
120
178
|
element.appendChild(childElement);
|
|
121
179
|
}
|
|
122
180
|
}
|
|
@@ -127,7 +185,7 @@ export class A2UIRenderer {
|
|
|
127
185
|
/**
|
|
128
186
|
* Create custom AfriCode component
|
|
129
187
|
*/
|
|
130
|
-
|
|
188
|
+
createCustomComponent({ tag, ...props }) {
|
|
131
189
|
const element = document.createElement(tag || 'div');
|
|
132
190
|
Object.assign(element, props);
|
|
133
191
|
return element;
|
|
@@ -136,7 +194,7 @@ export class A2UIRenderer {
|
|
|
136
194
|
/**
|
|
137
195
|
* Create form component
|
|
138
196
|
*/
|
|
139
|
-
|
|
197
|
+
createForm({ fields = [], onSubmit }) {
|
|
140
198
|
const form = document.createElement('af-form');
|
|
141
199
|
|
|
142
200
|
for (const field of fields) {
|
|
@@ -157,11 +215,11 @@ export class A2UIRenderer {
|
|
|
157
215
|
/**
|
|
158
216
|
* Create list component
|
|
159
217
|
*/
|
|
160
|
-
|
|
218
|
+
createList({ items = [] }) {
|
|
161
219
|
const list = document.createElement('af-grid');
|
|
162
220
|
|
|
163
221
|
for (const item of items) {
|
|
164
|
-
const card =
|
|
222
|
+
const card = this.createCard(item);
|
|
165
223
|
list.appendChild(card);
|
|
166
224
|
}
|
|
167
225
|
|
|
@@ -171,7 +229,7 @@ export class A2UIRenderer {
|
|
|
171
229
|
/**
|
|
172
230
|
* Create card component
|
|
173
231
|
*/
|
|
174
|
-
|
|
232
|
+
createCard({ title, content, image }) {
|
|
175
233
|
const card = document.createElement('af-card');
|
|
176
234
|
|
|
177
235
|
if (title) {
|
|
@@ -199,7 +257,7 @@ export class A2UIRenderer {
|
|
|
199
257
|
/**
|
|
200
258
|
* Create modal component
|
|
201
259
|
*/
|
|
202
|
-
|
|
260
|
+
createModal({ title, content, actions = [] }) {
|
|
203
261
|
const modal = document.createElement('af-modal');
|
|
204
262
|
modal.setAttribute('open', '');
|
|
205
263
|
|
|
@@ -238,7 +296,7 @@ export class A2UIRenderer {
|
|
|
238
296
|
/**
|
|
239
297
|
* Create toast component
|
|
240
298
|
*/
|
|
241
|
-
|
|
299
|
+
createToast({ message, type = 'info', duration = 3000 }) {
|
|
242
300
|
const toast = document.createElement('af-toast');
|
|
243
301
|
toast.setAttribute('type', type);
|
|
244
302
|
toast.textContent = message;
|
|
@@ -254,7 +312,7 @@ export class A2UIRenderer {
|
|
|
254
312
|
/**
|
|
255
313
|
* Create progress component
|
|
256
314
|
*/
|
|
257
|
-
|
|
315
|
+
createProgress({ value = 0, max = 100, label }) {
|
|
258
316
|
const progress = document.createElement('af-progress');
|
|
259
317
|
progress.setAttribute('value', value);
|
|
260
318
|
progress.setAttribute('max', max);
|
|
@@ -269,7 +327,7 @@ export class A2UIRenderer {
|
|
|
269
327
|
/**
|
|
270
328
|
* Update existing component
|
|
271
329
|
*/
|
|
272
|
-
|
|
330
|
+
update(id, newProps) {
|
|
273
331
|
const element = this.components.get(id);
|
|
274
332
|
if (element) {
|
|
275
333
|
Object.assign(element, newProps);
|
|
@@ -314,7 +372,7 @@ export class A2UIProtocol {
|
|
|
314
372
|
* Send a message into the A2UI system
|
|
315
373
|
* @param {Object|string} message - A2UI message
|
|
316
374
|
*/
|
|
317
|
-
|
|
375
|
+
sendMessage(message) {
|
|
318
376
|
return this.processMessage(message);
|
|
319
377
|
}
|
|
320
378
|
|
|
@@ -322,28 +380,31 @@ export class A2UIProtocol {
|
|
|
322
380
|
* Process incoming A2UI message
|
|
323
381
|
* @param {Object|string} message - A2UI message
|
|
324
382
|
*/
|
|
325
|
-
|
|
383
|
+
processMessage(message) {
|
|
326
384
|
if (typeof message === 'string') {
|
|
327
385
|
try {
|
|
328
386
|
message = JSON.parse(message);
|
|
329
387
|
} catch {
|
|
330
388
|
console.warn('Invalid A2UI message format:', message);
|
|
331
|
-
return;
|
|
389
|
+
return null;
|
|
332
390
|
}
|
|
333
391
|
}
|
|
334
392
|
|
|
335
|
-
this.
|
|
336
|
-
|
|
393
|
+
if (message?.type === 'render' && this.renderer) {
|
|
394
|
+
return this.renderer.render(message);
|
|
395
|
+
}
|
|
337
396
|
|
|
338
397
|
for (const handler of this.messageHandlers) {
|
|
339
398
|
handler(message);
|
|
340
399
|
}
|
|
400
|
+
|
|
401
|
+
return message;
|
|
341
402
|
}
|
|
342
403
|
|
|
343
404
|
/**
|
|
344
405
|
* Process queued messages
|
|
345
406
|
*/
|
|
346
|
-
|
|
407
|
+
processQueue() {
|
|
347
408
|
if (this.isProcessing || this.messageQueue.length === 0) {
|
|
348
409
|
return;
|
|
349
410
|
}
|
|
@@ -354,7 +415,7 @@ export class A2UIProtocol {
|
|
|
354
415
|
const message = this.messageQueue.shift();
|
|
355
416
|
|
|
356
417
|
try {
|
|
357
|
-
|
|
418
|
+
this.renderer.render(message);
|
|
358
419
|
} catch (error) {
|
|
359
420
|
console.error('A2UI render error:', error);
|
|
360
421
|
}
|
|
@@ -388,16 +449,41 @@ export class A2UIProtocol {
|
|
|
388
449
|
|
|
389
450
|
for (const message of messages) {
|
|
390
451
|
if (message.trim()) {
|
|
391
|
-
|
|
452
|
+
this.processMessage(message.trim());
|
|
392
453
|
}
|
|
393
454
|
}
|
|
394
455
|
}
|
|
395
456
|
|
|
396
457
|
// Process any remaining buffer
|
|
397
458
|
if (buffer.trim()) {
|
|
398
|
-
|
|
459
|
+
this.processMessage(buffer.trim());
|
|
399
460
|
}
|
|
400
461
|
}
|
|
462
|
+
|
|
463
|
+
createStream() {
|
|
464
|
+
const subscribers = new Set();
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
subscribe(callback) {
|
|
468
|
+
if (typeof callback !== 'function') {
|
|
469
|
+
return () => {};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
subscribers.add(callback);
|
|
473
|
+
return () => subscribers.delete(callback);
|
|
474
|
+
},
|
|
475
|
+
push(message) {
|
|
476
|
+
for (const callback of subscribers) {
|
|
477
|
+
callback(message);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
validateAgainstSchema(component) {
|
|
484
|
+
const tag = component?.tag || component?.tagName;
|
|
485
|
+
return Promise.resolve(Boolean(tag && coreComponentMap[tag]));
|
|
486
|
+
}
|
|
401
487
|
}
|
|
402
488
|
|
|
403
489
|
/**
|
|
@@ -423,9 +509,11 @@ export function initA2UI(root) {
|
|
|
423
509
|
update: (id, props) => renderer.update(id, props),
|
|
424
510
|
remove: (id) => renderer.remove(id),
|
|
425
511
|
sendMessage: (message) => protocol.sendMessage(message),
|
|
426
|
-
onMessage: (callback) => protocol.onMessage(callback)
|
|
512
|
+
onMessage: (callback) => protocol.onMessage(callback),
|
|
513
|
+
createStream: () => protocol.createStream(),
|
|
514
|
+
validateAgainstSchema: (component) => protocol.validateAgainstSchema(component)
|
|
427
515
|
};
|
|
428
516
|
}
|
|
429
517
|
|
|
430
518
|
return { renderer, protocol };
|
|
431
|
-
}
|
|
519
|
+
}
|
package/core/actions.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-aware action helpers for AfriCode.
|
|
3
|
+
*
|
|
4
|
+
* @module core/actions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { sessionStore } from './session-store.js';
|
|
8
|
+
|
|
9
|
+
export const actions = {
|
|
10
|
+
incrementCounter(sessionId) {
|
|
11
|
+
const session = sessionStore.get(sessionId);
|
|
12
|
+
const current = session.counter?.value || 0;
|
|
13
|
+
const nextValue = current + 1;
|
|
14
|
+
sessionStore.set(sessionId, 'counter/value', nextValue);
|
|
15
|
+
return nextValue;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
resetCounter(sessionId) {
|
|
19
|
+
sessionStore.set(sessionId, 'counter/value', 0);
|
|
20
|
+
return 0;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
getCounter(sessionId) {
|
|
24
|
+
const session = sessionStore.get(sessionId);
|
|
25
|
+
return session.counter?.value || 0;
|
|
26
|
+
}
|
|
27
|
+
};
|