freesail 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -5
- package/docs/A2UX_Protocol.md +183 -0
- package/docs/Agents.md +218 -0
- package/docs/Architecture.md +285 -0
- package/docs/CatalogReference.md +377 -0
- package/docs/GettingStarted.md +230 -0
- package/examples/demo/package.json +21 -0
- package/examples/demo/public/index.html +381 -0
- package/examples/demo/server.js +253 -0
- package/package.json +38 -5
- package/packages/core/package.json +48 -0
- package/packages/core/src/functions.ts +403 -0
- package/packages/core/src/index.ts +214 -0
- package/packages/core/src/parser.ts +270 -0
- package/packages/core/src/protocol.ts +254 -0
- package/packages/core/src/store.ts +452 -0
- package/packages/core/src/transport.ts +439 -0
- package/packages/core/src/types.ts +209 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/lit-ui/package.json +44 -0
- package/packages/lit-ui/src/catalogs/standard/catalog.json +405 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Badge.ts +96 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Button.ts +147 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Card.ts +78 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Checkbox.ts +94 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Column.ts +66 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Divider.ts +59 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Image.ts +54 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Input.ts +125 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Progress.ts +79 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Row.ts +68 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Select.ts +110 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Spacer.ts +37 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Spinner.ts +76 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Text.ts +86 -0
- package/packages/lit-ui/src/catalogs/standard/elements/index.ts +18 -0
- package/packages/lit-ui/src/catalogs/standard/index.ts +17 -0
- package/packages/lit-ui/src/index.ts +84 -0
- package/packages/lit-ui/src/renderer.ts +211 -0
- package/packages/lit-ui/src/types.ts +49 -0
- package/packages/lit-ui/src/utils/define-props.ts +157 -0
- package/packages/lit-ui/src/utils/index.ts +2 -0
- package/packages/lit-ui/src/utils/registry.ts +139 -0
- package/packages/lit-ui/tsconfig.json +11 -0
- package/packages/server/package.json +61 -0
- package/packages/server/src/adapters/index.ts +5 -0
- package/packages/server/src/adapters/langchain.ts +175 -0
- package/packages/server/src/adapters/openai.ts +209 -0
- package/packages/server/src/catalog-loader.ts +311 -0
- package/packages/server/src/index.ts +142 -0
- package/packages/server/src/stream.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/tsconfig.base.json +23 -0
- package/index.js +0 -3
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport Layer
|
|
3
|
+
*
|
|
4
|
+
* Handles SSE connections with automatic reconnection,
|
|
5
|
+
* action queueing, and offline support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { A2UXMessage, UserActionMessage, WatchSurfaceResponseMessage } from './types.js';
|
|
9
|
+
import { SSEParser } from './parser.js';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface TransportOptions {
|
|
16
|
+
/** Base URL for the SSE endpoint */
|
|
17
|
+
url: string;
|
|
18
|
+
/** Custom headers for requests */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
/** Enable automatic reconnection (default: true) */
|
|
21
|
+
autoReconnect?: boolean;
|
|
22
|
+
/** Initial reconnect delay in ms (default: 1000) */
|
|
23
|
+
reconnectDelay?: number;
|
|
24
|
+
/** Maximum reconnect delay in ms (default: 30000) */
|
|
25
|
+
maxReconnectDelay?: number;
|
|
26
|
+
/** Maximum reconnection attempts (default: Infinity) */
|
|
27
|
+
maxReconnectAttempts?: number;
|
|
28
|
+
/** Enable offline action queueing (default: true) */
|
|
29
|
+
offlineQueue?: boolean;
|
|
30
|
+
/** Debug mode */
|
|
31
|
+
debug?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TransportEventMap {
|
|
35
|
+
'message': A2UXMessage;
|
|
36
|
+
'connected': { url: string };
|
|
37
|
+
'disconnected': { reason: string };
|
|
38
|
+
'reconnecting': { attempt: number; delay: number };
|
|
39
|
+
'error': { message: string; originalError?: Error };
|
|
40
|
+
'queuedAction': { action: UserActionMessage | WatchSurfaceResponseMessage };
|
|
41
|
+
'flushedActions': { count: number };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TransportEventHandler<K extends keyof TransportEventMap> =
|
|
45
|
+
(data: TransportEventMap[K]) => void;
|
|
46
|
+
|
|
47
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// IndexedDB Action Queue (for offline support)
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
const DB_NAME = 'freesail_action_queue';
|
|
54
|
+
const STORE_NAME = 'actions';
|
|
55
|
+
|
|
56
|
+
class ActionQueue {
|
|
57
|
+
private db: IDBDatabase | null = null;
|
|
58
|
+
private memoryQueue: (UserActionMessage | WatchSurfaceResponseMessage)[] = [];
|
|
59
|
+
|
|
60
|
+
async init(): Promise<void> {
|
|
61
|
+
if (typeof indexedDB === 'undefined') {
|
|
62
|
+
// Fall back to memory queue in non-browser environments
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
68
|
+
|
|
69
|
+
request.onerror = () => reject(request.error);
|
|
70
|
+
|
|
71
|
+
request.onsuccess = () => {
|
|
72
|
+
this.db = request.result;
|
|
73
|
+
resolve();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
request.onupgradeneeded = (event) => {
|
|
77
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
78
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
79
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async enqueue(action: UserActionMessage | WatchSurfaceResponseMessage): Promise<void> {
|
|
86
|
+
if (!this.db) {
|
|
87
|
+
this.memoryQueue.push(action);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
|
93
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
94
|
+
const request = store.add({ action, timestamp: Date.now() });
|
|
95
|
+
|
|
96
|
+
request.onsuccess = () => resolve();
|
|
97
|
+
request.onerror = () => reject(request.error);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async dequeueAll(): Promise<(UserActionMessage | WatchSurfaceResponseMessage)[]> {
|
|
102
|
+
if (!this.db) {
|
|
103
|
+
const actions = [...this.memoryQueue];
|
|
104
|
+
this.memoryQueue = [];
|
|
105
|
+
return actions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
|
110
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
111
|
+
const request = store.getAll();
|
|
112
|
+
|
|
113
|
+
request.onsuccess = () => {
|
|
114
|
+
const items = request.result as { id: number; action: UserActionMessage | WatchSurfaceResponseMessage }[];
|
|
115
|
+
const actions = items.map(item => item.action);
|
|
116
|
+
|
|
117
|
+
// Clear the store
|
|
118
|
+
const clearRequest = store.clear();
|
|
119
|
+
clearRequest.onsuccess = () => resolve(actions);
|
|
120
|
+
clearRequest.onerror = () => reject(clearRequest.error);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
request.onerror = () => reject(request.error);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async getCount(): Promise<number> {
|
|
128
|
+
if (!this.db) {
|
|
129
|
+
return this.memoryQueue.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
|
|
134
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
135
|
+
const request = store.count();
|
|
136
|
+
|
|
137
|
+
request.onsuccess = () => resolve(request.result);
|
|
138
|
+
request.onerror = () => reject(request.error);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// SSE Transport
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* FreesailTransport manages SSE connections with resilience features:
|
|
149
|
+
* - Automatic reconnection with exponential backoff
|
|
150
|
+
* - Offline action queueing via IndexedDB
|
|
151
|
+
* - Connection state management
|
|
152
|
+
*/
|
|
153
|
+
export class FreesailTransport {
|
|
154
|
+
private options: Required<TransportOptions>;
|
|
155
|
+
private eventSource: EventSource | null = null;
|
|
156
|
+
private parser: SSEParser;
|
|
157
|
+
private actionQueue: ActionQueue;
|
|
158
|
+
private listeners: Map<keyof TransportEventMap, Set<TransportEventHandler<keyof TransportEventMap>>> = new Map();
|
|
159
|
+
|
|
160
|
+
private _state: ConnectionState = 'disconnected';
|
|
161
|
+
private reconnectAttempt: number = 0;
|
|
162
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
163
|
+
|
|
164
|
+
constructor(options: TransportOptions) {
|
|
165
|
+
this.options = {
|
|
166
|
+
url: options.url,
|
|
167
|
+
headers: options.headers ?? {},
|
|
168
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
169
|
+
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
170
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 30000,
|
|
171
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? Infinity,
|
|
172
|
+
offlineQueue: options.offlineQueue ?? true,
|
|
173
|
+
debug: options.debug ?? false,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
this.parser = new SSEParser();
|
|
177
|
+
this.actionQueue = new ActionQueue();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
// Connection Management
|
|
182
|
+
// ===========================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Connect to the SSE endpoint
|
|
186
|
+
*/
|
|
187
|
+
async connect(): Promise<void> {
|
|
188
|
+
if (this._state === 'connected' || this._state === 'connecting') {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Initialize action queue
|
|
193
|
+
if (this.options.offlineQueue) {
|
|
194
|
+
await this.actionQueue.init();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.setState('connecting');
|
|
198
|
+
this.createEventSource();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Disconnect from the SSE endpoint
|
|
203
|
+
*/
|
|
204
|
+
disconnect(): void {
|
|
205
|
+
this.cancelReconnect();
|
|
206
|
+
this.closeEventSource();
|
|
207
|
+
this.setState('disconnected');
|
|
208
|
+
this.emit('disconnected', { reason: 'manual' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get current connection state
|
|
213
|
+
*/
|
|
214
|
+
get state(): ConnectionState {
|
|
215
|
+
return this._state;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if connected
|
|
220
|
+
*/
|
|
221
|
+
get isConnected(): boolean {
|
|
222
|
+
return this._state === 'connected';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
// Sending Messages
|
|
227
|
+
// ===========================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Send a user action to the server
|
|
231
|
+
*/
|
|
232
|
+
async sendAction(action: UserActionMessage): Promise<void> {
|
|
233
|
+
await this.sendMessage(action);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Send a watch surface response
|
|
238
|
+
*/
|
|
239
|
+
async sendWatchResponse(response: WatchSurfaceResponseMessage): Promise<void> {
|
|
240
|
+
await this.sendMessage(response);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private async sendMessage(message: UserActionMessage | WatchSurfaceResponseMessage): Promise<void> {
|
|
244
|
+
if (!this.isConnected) {
|
|
245
|
+
if (this.options.offlineQueue) {
|
|
246
|
+
await this.actionQueue.enqueue(message);
|
|
247
|
+
this.emit('queuedAction', { action: message });
|
|
248
|
+
this.log(`Action queued for later: ${JSON.stringify(message)}`);
|
|
249
|
+
} else {
|
|
250
|
+
throw new Error('Not connected and offline queue is disabled');
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(this.options.url, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: {
|
|
259
|
+
'Content-Type': 'application/json',
|
|
260
|
+
...this.options.headers,
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify(message),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
270
|
+
|
|
271
|
+
// Queue for retry if offline
|
|
272
|
+
if (this.options.offlineQueue) {
|
|
273
|
+
await this.actionQueue.enqueue(message);
|
|
274
|
+
this.emit('queuedAction', { action: message });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.emit('error', { message: err.message, originalError: err });
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ===========================================================================
|
|
283
|
+
// Event Handling
|
|
284
|
+
// ===========================================================================
|
|
285
|
+
|
|
286
|
+
on<K extends keyof TransportEventMap>(
|
|
287
|
+
event: K,
|
|
288
|
+
handler: TransportEventHandler<K>
|
|
289
|
+
): () => void {
|
|
290
|
+
if (!this.listeners.has(event)) {
|
|
291
|
+
this.listeners.set(event, new Set());
|
|
292
|
+
}
|
|
293
|
+
this.listeners.get(event)!.add(handler as TransportEventHandler<keyof TransportEventMap>);
|
|
294
|
+
|
|
295
|
+
return () => this.off(event, handler);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
off<K extends keyof TransportEventMap>(
|
|
299
|
+
event: K,
|
|
300
|
+
handler: TransportEventHandler<K>
|
|
301
|
+
): void {
|
|
302
|
+
this.listeners.get(event)?.delete(handler as TransportEventHandler<keyof TransportEventMap>);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private emit<K extends keyof TransportEventMap>(event: K, data: TransportEventMap[K]): void {
|
|
306
|
+
this.listeners.get(event)?.forEach(handler => {
|
|
307
|
+
try {
|
|
308
|
+
(handler as TransportEventHandler<K>)(data);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error(`Error in transport event handler for ${event}:`, error);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ===========================================================================
|
|
316
|
+
// Private: EventSource Management
|
|
317
|
+
// ===========================================================================
|
|
318
|
+
|
|
319
|
+
private createEventSource(): void {
|
|
320
|
+
try {
|
|
321
|
+
this.eventSource = new EventSource(this.options.url);
|
|
322
|
+
|
|
323
|
+
this.eventSource.onopen = () => {
|
|
324
|
+
this.setState('connected');
|
|
325
|
+
this.reconnectAttempt = 0;
|
|
326
|
+
this.emit('connected', { url: this.options.url });
|
|
327
|
+
this.log('Connected to SSE endpoint');
|
|
328
|
+
|
|
329
|
+
// Flush queued actions
|
|
330
|
+
this.flushQueue();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
this.eventSource.onmessage = (event) => {
|
|
334
|
+
const messages = this.parser.feed(event.data);
|
|
335
|
+
for (const message of messages) {
|
|
336
|
+
this.emit('message', message);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this.eventSource.onerror = () => {
|
|
341
|
+
this.closeEventSource();
|
|
342
|
+
|
|
343
|
+
if (this.options.autoReconnect &&
|
|
344
|
+
this.reconnectAttempt < this.options.maxReconnectAttempts) {
|
|
345
|
+
this.scheduleReconnect();
|
|
346
|
+
} else {
|
|
347
|
+
this.setState('disconnected');
|
|
348
|
+
this.emit('disconnected', { reason: 'error' });
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
353
|
+
this.emit('error', { message: err.message, originalError: err });
|
|
354
|
+
this.setState('disconnected');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private closeEventSource(): void {
|
|
359
|
+
if (this.eventSource) {
|
|
360
|
+
this.eventSource.close();
|
|
361
|
+
this.eventSource = null;
|
|
362
|
+
}
|
|
363
|
+
this.parser.reset();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ===========================================================================
|
|
367
|
+
// Private: Reconnection Logic
|
|
368
|
+
// ===========================================================================
|
|
369
|
+
|
|
370
|
+
private scheduleReconnect(): void {
|
|
371
|
+
this.setState('reconnecting');
|
|
372
|
+
this.reconnectAttempt++;
|
|
373
|
+
|
|
374
|
+
// Exponential backoff with jitter
|
|
375
|
+
const baseDelay = Math.min(
|
|
376
|
+
this.options.reconnectDelay * Math.pow(2, this.reconnectAttempt - 1),
|
|
377
|
+
this.options.maxReconnectDelay
|
|
378
|
+
);
|
|
379
|
+
const jitter = Math.random() * 0.3 * baseDelay;
|
|
380
|
+
const delay = baseDelay + jitter;
|
|
381
|
+
|
|
382
|
+
this.emit('reconnecting', { attempt: this.reconnectAttempt, delay });
|
|
383
|
+
this.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempt})`);
|
|
384
|
+
|
|
385
|
+
this.reconnectTimer = setTimeout(() => {
|
|
386
|
+
this.createEventSource();
|
|
387
|
+
}, delay);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private cancelReconnect(): void {
|
|
391
|
+
if (this.reconnectTimer) {
|
|
392
|
+
clearTimeout(this.reconnectTimer);
|
|
393
|
+
this.reconnectTimer = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
// Private: Queue Management
|
|
399
|
+
// ===========================================================================
|
|
400
|
+
|
|
401
|
+
private async flushQueue(): Promise<void> {
|
|
402
|
+
if (!this.options.offlineQueue) return;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const actions = await this.actionQueue.dequeueAll();
|
|
406
|
+
|
|
407
|
+
if (actions.length > 0) {
|
|
408
|
+
this.log(`Flushing ${actions.length} queued actions`);
|
|
409
|
+
|
|
410
|
+
for (const action of actions) {
|
|
411
|
+
try {
|
|
412
|
+
await this.sendMessage(action);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
// Re-queue on failure
|
|
415
|
+
this.log(`Failed to flush action, re-queueing: ${error}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.emit('flushedActions', { count: actions.length });
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('Failed to flush action queue:', error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
// Private: State & Logging
|
|
428
|
+
// ===========================================================================
|
|
429
|
+
|
|
430
|
+
private setState(state: ConnectionState): void {
|
|
431
|
+
this._state = state;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private log(message: string): void {
|
|
435
|
+
if (this.options.debug) {
|
|
436
|
+
console.log(`[FreesailTransport] ${message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UX Protocol Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the complete type system for the A2UX protocol,
|
|
5
|
+
* an extension of Google A2UI for generative UI communication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Component Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base component definition used in updateComponents messages
|
|
14
|
+
*/
|
|
15
|
+
export interface A2UXComponent {
|
|
16
|
+
/** Unique identifier for this component within the surface */
|
|
17
|
+
id: string;
|
|
18
|
+
/** The component type from the catalog (e.g., 'Text', 'Button', 'Column') */
|
|
19
|
+
component: string;
|
|
20
|
+
/** Child component IDs (for container components) */
|
|
21
|
+
children?: string[];
|
|
22
|
+
/** Any additional properties defined in the catalog */
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Server to Client Messages (Downstream)
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* createSurface - Initializes a UI container and loads a specific Catalog
|
|
32
|
+
*/
|
|
33
|
+
export interface CreateSurfaceMessage {
|
|
34
|
+
createSurface: {
|
|
35
|
+
/** Unique identifier for the UI surface */
|
|
36
|
+
surfaceId: string;
|
|
37
|
+
/** Catalog identifier for component definitions */
|
|
38
|
+
catalogId: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* updateComponents - Streams structural UI definitions
|
|
44
|
+
*/
|
|
45
|
+
export interface UpdateComponentsMessage {
|
|
46
|
+
updateComponents: {
|
|
47
|
+
/** Target surface ID */
|
|
48
|
+
surfaceId: string;
|
|
49
|
+
/** Array of component definitions (flat adjacency list) */
|
|
50
|
+
components: A2UXComponent[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* updateDataModel - Pushes data/state changes to the UI
|
|
56
|
+
*/
|
|
57
|
+
export interface UpdateDataModelMessage {
|
|
58
|
+
updateDataModel: {
|
|
59
|
+
/** Target surface ID */
|
|
60
|
+
surfaceId: string;
|
|
61
|
+
/** JSON Pointer path for partial updates (optional) */
|
|
62
|
+
path?: string;
|
|
63
|
+
/** Operation type: 'add', 'replace', or 'remove' */
|
|
64
|
+
op?: 'add' | 'replace' | 'remove';
|
|
65
|
+
/** The data value (required for 'add' and 'replace') */
|
|
66
|
+
value?: unknown;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* deleteSurface - Removes a surface and all its components
|
|
72
|
+
*/
|
|
73
|
+
export interface DeleteSurfaceMessage {
|
|
74
|
+
deleteSurface: {
|
|
75
|
+
/** Surface ID to delete */
|
|
76
|
+
surfaceId: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* watchSurface - Configures automatic state reporting
|
|
82
|
+
*/
|
|
83
|
+
export interface WatchSurfaceMessage {
|
|
84
|
+
watchSurface: {
|
|
85
|
+
/** Target surface ID */
|
|
86
|
+
surfaceId: string;
|
|
87
|
+
/** Reporting interval in seconds (default: 10) */
|
|
88
|
+
interval?: number;
|
|
89
|
+
/** Watch expiration in seconds (default: 300) */
|
|
90
|
+
expiresIn?: number;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* unwatchSurface - Removes a surface watcher
|
|
96
|
+
*/
|
|
97
|
+
export interface UnwatchSurfaceMessage {
|
|
98
|
+
unwatchSurface: {
|
|
99
|
+
/** Surface ID to unwatch */
|
|
100
|
+
surfaceId: string;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Client to Server Messages (Upstream)
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* userAction - Reports user interactions to the server
|
|
110
|
+
*/
|
|
111
|
+
export interface UserActionMessage {
|
|
112
|
+
userAction: {
|
|
113
|
+
/** Source surface ID */
|
|
114
|
+
surfaceId: string;
|
|
115
|
+
/** Semantic action label (e.g., 'submit_form', 'button_click') */
|
|
116
|
+
action: string;
|
|
117
|
+
/** Full context/state relevant to the action */
|
|
118
|
+
context: Record<string, unknown>;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* watchSurfaceResponse - Automatic state report from watch
|
|
124
|
+
*/
|
|
125
|
+
export interface WatchSurfaceResponseMessage {
|
|
126
|
+
watchSurfaceResponse: {
|
|
127
|
+
/** Source surface ID */
|
|
128
|
+
surfaceId: string;
|
|
129
|
+
/** Current data model state (JSON Pointer paths as keys) */
|
|
130
|
+
data: Record<string, unknown>;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Union Types
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
/** All server-to-client message types */
|
|
139
|
+
export type ServerToClientMessage =
|
|
140
|
+
| CreateSurfaceMessage
|
|
141
|
+
| UpdateComponentsMessage
|
|
142
|
+
| UpdateDataModelMessage
|
|
143
|
+
| DeleteSurfaceMessage
|
|
144
|
+
| WatchSurfaceMessage
|
|
145
|
+
| UnwatchSurfaceMessage;
|
|
146
|
+
|
|
147
|
+
/** All client-to-server message types */
|
|
148
|
+
export type ClientToServerMessage =
|
|
149
|
+
| UserActionMessage
|
|
150
|
+
| WatchSurfaceResponseMessage;
|
|
151
|
+
|
|
152
|
+
/** Any A2UX protocol message */
|
|
153
|
+
export type A2UXMessage = ServerToClientMessage | ClientToServerMessage;
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// Type Guards
|
|
157
|
+
// =============================================================================
|
|
158
|
+
|
|
159
|
+
export function isCreateSurface(msg: A2UXMessage): msg is CreateSurfaceMessage {
|
|
160
|
+
return 'createSurface' in msg;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isUpdateComponents(msg: A2UXMessage): msg is UpdateComponentsMessage {
|
|
164
|
+
return 'updateComponents' in msg;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isUpdateDataModel(msg: A2UXMessage): msg is UpdateDataModelMessage {
|
|
168
|
+
return 'updateDataModel' in msg;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isDeleteSurface(msg: A2UXMessage): msg is DeleteSurfaceMessage {
|
|
172
|
+
return 'deleteSurface' in msg;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isWatchSurface(msg: A2UXMessage): msg is WatchSurfaceMessage {
|
|
176
|
+
return 'watchSurface' in msg;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isUnwatchSurface(msg: A2UXMessage): msg is UnwatchSurfaceMessage {
|
|
180
|
+
return 'unwatchSurface' in msg;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isUserAction(msg: A2UXMessage): msg is UserActionMessage {
|
|
184
|
+
return 'userAction' in msg;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function isWatchSurfaceResponse(msg: A2UXMessage): msg is WatchSurfaceResponseMessage {
|
|
188
|
+
return 'watchSurfaceResponse' in msg;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// =============================================================================
|
|
192
|
+
// Message Utilities
|
|
193
|
+
// =============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Gets the message type from an A2UX message
|
|
197
|
+
*/
|
|
198
|
+
export function getMessageType(msg: A2UXMessage): string {
|
|
199
|
+
return Object.keys(msg)[0];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gets the surfaceId from any A2UX message
|
|
204
|
+
*/
|
|
205
|
+
export function getSurfaceId(msg: A2UXMessage): string {
|
|
206
|
+
const key = getMessageType(msg);
|
|
207
|
+
const payload = (msg as unknown as Record<string, { surfaceId: string }>)[key];
|
|
208
|
+
return payload.surfaceId;
|
|
209
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@freesail/lit-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Freesail Lit UI - Web Components for A2UX Protocol",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./standard": {
|
|
15
|
+
"types": "./dist/catalogs/standard/index.d.ts",
|
|
16
|
+
"import": "./dist/catalogs/standard/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"lint": "eslint src --ext .ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@freesail/core": "^0.1.0",
|
|
31
|
+
"lit": "^3.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.3",
|
|
35
|
+
"vitest": "^1.2.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"web-components",
|
|
39
|
+
"lit",
|
|
40
|
+
"a2ux",
|
|
41
|
+
"generative-ui"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
}
|