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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Stream Parser
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing of JSON data from SSE streams,
|
|
5
|
+
* including incremental/chunked JSON parsing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { A2UXMessage } from './types.js';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface ParseResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
message?: A2UXMessage;
|
|
17
|
+
error?: string;
|
|
18
|
+
remaining?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ParserOptions {
|
|
22
|
+
/** Enable lenient parsing mode */
|
|
23
|
+
lenient?: boolean;
|
|
24
|
+
/** Maximum buffer size in characters */
|
|
25
|
+
maxBufferSize?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// JSON Stream Parser
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* JSONStreamParser handles incremental JSON parsing from SSE streams.
|
|
34
|
+
* It buffers partial JSON and emits complete messages.
|
|
35
|
+
*/
|
|
36
|
+
export class JSONStreamParser {
|
|
37
|
+
private buffer: string = '';
|
|
38
|
+
private maxBufferSize: number;
|
|
39
|
+
private lenient: boolean;
|
|
40
|
+
|
|
41
|
+
constructor(options: ParserOptions = {}) {
|
|
42
|
+
this.maxBufferSize = options.maxBufferSize ?? 1024 * 1024; // 1MB default
|
|
43
|
+
this.lenient = options.lenient ?? false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Feed data into the parser and get any complete messages
|
|
48
|
+
*/
|
|
49
|
+
feed(chunk: string): A2UXMessage[] {
|
|
50
|
+
this.buffer += chunk;
|
|
51
|
+
|
|
52
|
+
// Check buffer overflow
|
|
53
|
+
if (this.buffer.length > this.maxBufferSize) {
|
|
54
|
+
console.warn('[JSONStreamParser] Buffer overflow, clearing buffer');
|
|
55
|
+
this.buffer = '';
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this.extractMessages();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a single complete JSON string
|
|
64
|
+
*/
|
|
65
|
+
parse(jsonString: string): ParseResult {
|
|
66
|
+
try {
|
|
67
|
+
const message = JSON.parse(jsonString) as A2UXMessage;
|
|
68
|
+
return { success: true, message };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: err.message,
|
|
74
|
+
remaining: jsonString
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reset the parser state
|
|
81
|
+
*/
|
|
82
|
+
reset(): void {
|
|
83
|
+
this.buffer = '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get current buffer content (for debugging)
|
|
88
|
+
*/
|
|
89
|
+
getBuffer(): string {
|
|
90
|
+
return this.buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ===========================================================================
|
|
94
|
+
// Private Methods
|
|
95
|
+
// ===========================================================================
|
|
96
|
+
|
|
97
|
+
private extractMessages(): A2UXMessage[] {
|
|
98
|
+
const messages: A2UXMessage[] = [];
|
|
99
|
+
|
|
100
|
+
// Try to extract complete JSON objects
|
|
101
|
+
let startIndex = 0;
|
|
102
|
+
let depth = 0;
|
|
103
|
+
let inString = false;
|
|
104
|
+
let escapeNext = false;
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
107
|
+
const char = this.buffer[i];
|
|
108
|
+
|
|
109
|
+
if (escapeNext) {
|
|
110
|
+
escapeNext = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (char === '\\' && inString) {
|
|
115
|
+
escapeNext = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (char === '"') {
|
|
120
|
+
inString = !inString;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (inString) continue;
|
|
125
|
+
|
|
126
|
+
if (char === '{') {
|
|
127
|
+
if (depth === 0) {
|
|
128
|
+
startIndex = i;
|
|
129
|
+
}
|
|
130
|
+
depth++;
|
|
131
|
+
} else if (char === '}') {
|
|
132
|
+
depth--;
|
|
133
|
+
if (depth === 0) {
|
|
134
|
+
// Found complete JSON object
|
|
135
|
+
const jsonString = this.buffer.slice(startIndex, i + 1);
|
|
136
|
+
const result = this.parse(jsonString);
|
|
137
|
+
|
|
138
|
+
if (result.success && result.message) {
|
|
139
|
+
messages.push(result.message);
|
|
140
|
+
} else if (!this.lenient) {
|
|
141
|
+
console.warn('[JSONStreamParser] Failed to parse:', result.error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
startIndex = i + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Keep remaining incomplete data in buffer
|
|
150
|
+
this.buffer = this.buffer.slice(startIndex);
|
|
151
|
+
|
|
152
|
+
return messages;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// =============================================================================
|
|
157
|
+
// SSE Parser
|
|
158
|
+
// =============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* SSEParser handles Server-Sent Events format parsing
|
|
162
|
+
*/
|
|
163
|
+
export class SSEParser {
|
|
164
|
+
private jsonParser: JSONStreamParser;
|
|
165
|
+
private lineBuffer: string = '';
|
|
166
|
+
|
|
167
|
+
constructor(options: ParserOptions = {}) {
|
|
168
|
+
this.jsonParser = new JSONStreamParser(options);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Feed raw SSE data and extract A2UX messages
|
|
173
|
+
*/
|
|
174
|
+
feed(chunk: string): A2UXMessage[] {
|
|
175
|
+
const messages: A2UXMessage[] = [];
|
|
176
|
+
this.lineBuffer += chunk;
|
|
177
|
+
|
|
178
|
+
// Split by SSE line endings
|
|
179
|
+
const lines = this.lineBuffer.split(/\r?\n/);
|
|
180
|
+
|
|
181
|
+
// Keep the last incomplete line in buffer
|
|
182
|
+
this.lineBuffer = lines.pop() ?? '';
|
|
183
|
+
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
|
|
187
|
+
// Skip empty lines and comments
|
|
188
|
+
if (!trimmed || trimmed.startsWith(':')) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Parse SSE data field
|
|
193
|
+
if (trimmed.startsWith('data:')) {
|
|
194
|
+
const data = trimmed.slice(5).trim();
|
|
195
|
+
|
|
196
|
+
// Skip [DONE] marker
|
|
197
|
+
if (data === '[DONE]') {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const parsed = this.jsonParser.feed(data);
|
|
202
|
+
messages.push(...parsed);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return messages;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Reset the parser state
|
|
211
|
+
*/
|
|
212
|
+
reset(): void {
|
|
213
|
+
this.jsonParser.reset();
|
|
214
|
+
this.lineBuffer = '';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Template Expression Parser
|
|
220
|
+
// =============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Parses and evaluates template expressions like ${user.name}
|
|
224
|
+
*/
|
|
225
|
+
export function parseTemplateExpression(
|
|
226
|
+
template: string,
|
|
227
|
+
dataModel: Record<string, unknown>
|
|
228
|
+
): string {
|
|
229
|
+
return template.replace(/\$\{([^}]+)\}/g, (_, expression) => {
|
|
230
|
+
try {
|
|
231
|
+
const value = evaluateExpression(expression.trim(), dataModel);
|
|
232
|
+
return String(value ?? '');
|
|
233
|
+
} catch {
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Evaluates a simple path expression against a data model
|
|
241
|
+
*/
|
|
242
|
+
function evaluateExpression(
|
|
243
|
+
expression: string,
|
|
244
|
+
dataModel: Record<string, unknown>
|
|
245
|
+
): unknown {
|
|
246
|
+
const parts = expression.split('.');
|
|
247
|
+
let current: unknown = dataModel;
|
|
248
|
+
|
|
249
|
+
for (const part of parts) {
|
|
250
|
+
if (current === null || current === undefined) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle array index access like items[0]
|
|
255
|
+
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
256
|
+
if (arrayMatch) {
|
|
257
|
+
const [, key, index] = arrayMatch;
|
|
258
|
+
current = (current as Record<string, unknown>)[key];
|
|
259
|
+
if (Array.isArray(current)) {
|
|
260
|
+
current = current[parseInt(index, 10)];
|
|
261
|
+
} else {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
current = (current as Record<string, unknown>)[part];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return current;
|
|
270
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UX Protocol Handler
|
|
3
|
+
*
|
|
4
|
+
* Core protocol listener that processes incoming A2UX messages
|
|
5
|
+
* and dispatches them to appropriate handlers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
A2UXMessage,
|
|
10
|
+
CreateSurfaceMessage,
|
|
11
|
+
UpdateComponentsMessage,
|
|
12
|
+
UpdateDataModelMessage,
|
|
13
|
+
DeleteSurfaceMessage,
|
|
14
|
+
WatchSurfaceMessage,
|
|
15
|
+
UnwatchSurfaceMessage,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
isCreateSurface,
|
|
20
|
+
isUpdateComponents,
|
|
21
|
+
isUpdateDataModel,
|
|
22
|
+
isDeleteSurface,
|
|
23
|
+
isWatchSurface,
|
|
24
|
+
isUnwatchSurface,
|
|
25
|
+
getMessageType,
|
|
26
|
+
} from './types.js';
|
|
27
|
+
|
|
28
|
+
import { SurfaceStore } from './store';
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Event Types
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export interface ProtocolEventMap {
|
|
35
|
+
'createSurface': CreateSurfaceMessage['createSurface'];
|
|
36
|
+
'updateComponents': UpdateComponentsMessage['updateComponents'];
|
|
37
|
+
'updateDataModel': UpdateDataModelMessage['updateDataModel'];
|
|
38
|
+
'deleteSurface': DeleteSurfaceMessage['deleteSurface'];
|
|
39
|
+
'watchSurface': WatchSurfaceMessage['watchSurface'];
|
|
40
|
+
'unwatchSurface': UnwatchSurfaceMessage['unwatchSurface'];
|
|
41
|
+
'error': { message: string; originalError?: Error };
|
|
42
|
+
'connected': { url: string };
|
|
43
|
+
'disconnected': { reason: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ProtocolEventHandler<K extends keyof ProtocolEventMap> =
|
|
47
|
+
(data: ProtocolEventMap[K]) => void;
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Protocol Handler
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
export interface ProtocolHandlerOptions {
|
|
54
|
+
/** Surface store for state management */
|
|
55
|
+
store?: SurfaceStore;
|
|
56
|
+
/** Enable debug logging */
|
|
57
|
+
debug?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A2UXProtocolHandler processes incoming A2UX messages and manages
|
|
62
|
+
* protocol state. It acts as the central message dispatcher.
|
|
63
|
+
*/
|
|
64
|
+
export class A2UXProtocolHandler {
|
|
65
|
+
private store: SurfaceStore;
|
|
66
|
+
private debug: boolean;
|
|
67
|
+
private listeners: Map<keyof ProtocolEventMap, Set<ProtocolEventHandler<keyof ProtocolEventMap>>> = new Map();
|
|
68
|
+
private watchTimers: Map<string, ReturnType<typeof setInterval>> = new Map();
|
|
69
|
+
|
|
70
|
+
constructor(options: ProtocolHandlerOptions = {}) {
|
|
71
|
+
this.store = options.store ?? new SurfaceStore();
|
|
72
|
+
this.debug = options.debug ?? false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Process an incoming A2UX message
|
|
77
|
+
*/
|
|
78
|
+
processMessage(message: A2UXMessage): void {
|
|
79
|
+
try {
|
|
80
|
+
const messageType = getMessageType(message);
|
|
81
|
+
this.log(`Processing message: ${messageType}`);
|
|
82
|
+
|
|
83
|
+
if (isCreateSurface(message)) {
|
|
84
|
+
this.handleCreateSurface(message.createSurface);
|
|
85
|
+
} else if (isUpdateComponents(message)) {
|
|
86
|
+
this.handleUpdateComponents(message.updateComponents);
|
|
87
|
+
} else if (isUpdateDataModel(message)) {
|
|
88
|
+
this.handleUpdateDataModel(message.updateDataModel);
|
|
89
|
+
} else if (isDeleteSurface(message)) {
|
|
90
|
+
this.handleDeleteSurface(message.deleteSurface);
|
|
91
|
+
} else if (isWatchSurface(message)) {
|
|
92
|
+
this.handleWatchSurface(message.watchSurface);
|
|
93
|
+
} else if (isUnwatchSurface(message)) {
|
|
94
|
+
this.handleUnwatchSurface(message.unwatchSurface);
|
|
95
|
+
} else {
|
|
96
|
+
this.log(`Unknown message type: ${messageType}`, 'warn');
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
100
|
+
this.emit('error', { message: err.message, originalError: err });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Process a raw JSON string message
|
|
106
|
+
*/
|
|
107
|
+
processRawMessage(jsonString: string): void {
|
|
108
|
+
try {
|
|
109
|
+
const message = JSON.parse(jsonString) as A2UXMessage;
|
|
110
|
+
this.processMessage(message);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
113
|
+
this.emit('error', {
|
|
114
|
+
message: `Failed to parse message: ${err.message}`,
|
|
115
|
+
originalError: err
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ===========================================================================
|
|
121
|
+
// Message Handlers
|
|
122
|
+
// ===========================================================================
|
|
123
|
+
|
|
124
|
+
private handleCreateSurface(payload: CreateSurfaceMessage['createSurface']): void {
|
|
125
|
+
const { surfaceId, catalogId } = payload;
|
|
126
|
+
this.store.createSurface(surfaceId, catalogId);
|
|
127
|
+
this.emit('createSurface', payload);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private handleUpdateComponents(payload: UpdateComponentsMessage['updateComponents']): void {
|
|
131
|
+
const { surfaceId, components } = payload;
|
|
132
|
+
this.store.updateComponents(surfaceId, components);
|
|
133
|
+
this.emit('updateComponents', payload);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private handleUpdateDataModel(payload: UpdateDataModelMessage['updateDataModel']): void {
|
|
137
|
+
const { surfaceId, path, op, value } = payload;
|
|
138
|
+
this.store.updateDataModel(surfaceId, path ?? '/', op ?? 'replace', value);
|
|
139
|
+
this.emit('updateDataModel', payload);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private handleDeleteSurface(payload: DeleteSurfaceMessage['deleteSurface']): void {
|
|
143
|
+
const { surfaceId } = payload;
|
|
144
|
+
|
|
145
|
+
// Clear any active watchers
|
|
146
|
+
this.clearWatcher(surfaceId);
|
|
147
|
+
|
|
148
|
+
this.store.deleteSurface(surfaceId);
|
|
149
|
+
this.emit('deleteSurface', payload);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleWatchSurface(payload: WatchSurfaceMessage['watchSurface']): void {
|
|
153
|
+
const { surfaceId, interval = 10, expiresIn = 300 } = payload;
|
|
154
|
+
|
|
155
|
+
// Clear any existing watcher
|
|
156
|
+
this.clearWatcher(surfaceId);
|
|
157
|
+
|
|
158
|
+
// Set up new watcher
|
|
159
|
+
const timer = setInterval(() => {
|
|
160
|
+
this.store.triggerWatchCallback(surfaceId);
|
|
161
|
+
}, interval * 1000);
|
|
162
|
+
|
|
163
|
+
this.watchTimers.set(surfaceId, timer);
|
|
164
|
+
|
|
165
|
+
// Set expiration
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
this.clearWatcher(surfaceId);
|
|
168
|
+
}, expiresIn * 1000);
|
|
169
|
+
|
|
170
|
+
this.emit('watchSurface', payload);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private handleUnwatchSurface(payload: UnwatchSurfaceMessage['unwatchSurface']): void {
|
|
174
|
+
const { surfaceId } = payload;
|
|
175
|
+
this.clearWatcher(surfaceId);
|
|
176
|
+
this.emit('unwatchSurface', payload);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private clearWatcher(surfaceId: string): void {
|
|
180
|
+
const timer = this.watchTimers.get(surfaceId);
|
|
181
|
+
if (timer) {
|
|
182
|
+
clearInterval(timer);
|
|
183
|
+
this.watchTimers.delete(surfaceId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ===========================================================================
|
|
188
|
+
// Event Emitter
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
|
|
191
|
+
on<K extends keyof ProtocolEventMap>(
|
|
192
|
+
event: K,
|
|
193
|
+
handler: ProtocolEventHandler<K>
|
|
194
|
+
): () => void {
|
|
195
|
+
if (!this.listeners.has(event)) {
|
|
196
|
+
this.listeners.set(event, new Set());
|
|
197
|
+
}
|
|
198
|
+
this.listeners.get(event)!.add(handler as ProtocolEventHandler<keyof ProtocolEventMap>);
|
|
199
|
+
|
|
200
|
+
// Return unsubscribe function
|
|
201
|
+
return () => this.off(event, handler);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
off<K extends keyof ProtocolEventMap>(
|
|
205
|
+
event: K,
|
|
206
|
+
handler: ProtocolEventHandler<K>
|
|
207
|
+
): void {
|
|
208
|
+
this.listeners.get(event)?.delete(handler as ProtocolEventHandler<keyof ProtocolEventMap>);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private emit<K extends keyof ProtocolEventMap>(event: K, data: ProtocolEventMap[K]): void {
|
|
212
|
+
this.listeners.get(event)?.forEach(handler => {
|
|
213
|
+
try {
|
|
214
|
+
(handler as ProtocolEventHandler<K>)(data);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ===========================================================================
|
|
222
|
+
// Accessors
|
|
223
|
+
// ===========================================================================
|
|
224
|
+
|
|
225
|
+
getStore(): SurfaceStore {
|
|
226
|
+
return this.store;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ===========================================================================
|
|
230
|
+
// Cleanup
|
|
231
|
+
// ===========================================================================
|
|
232
|
+
|
|
233
|
+
destroy(): void {
|
|
234
|
+
// Clear all watchers
|
|
235
|
+
this.watchTimers.forEach((timer) => clearInterval(timer));
|
|
236
|
+
this.watchTimers.clear();
|
|
237
|
+
|
|
238
|
+
// Clear all listeners
|
|
239
|
+
this.listeners.clear();
|
|
240
|
+
|
|
241
|
+
// Clear store
|
|
242
|
+
this.store.clear();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ===========================================================================
|
|
246
|
+
// Debug
|
|
247
|
+
// ===========================================================================
|
|
248
|
+
|
|
249
|
+
private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void {
|
|
250
|
+
if (this.debug) {
|
|
251
|
+
console[level](`[A2UX Protocol] ${message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|