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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE Stream Implementation
|
|
3
|
+
*
|
|
4
|
+
* Manages Server-Sent Events connections with backpressure handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ServerResponse } from 'http';
|
|
8
|
+
import type { ServerToClientMessage } from '@freesail/core';
|
|
9
|
+
|
|
10
|
+
export interface StreamOptions {
|
|
11
|
+
/** Response object to write to */
|
|
12
|
+
response: ServerResponse;
|
|
13
|
+
/** Session/connection identifier */
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
/** Keep-alive interval in ms (default: 15000) */
|
|
16
|
+
keepAliveInterval?: number;
|
|
17
|
+
/** Enable debug logging */
|
|
18
|
+
debug?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StreamStats {
|
|
22
|
+
messagesSent: number;
|
|
23
|
+
bytesWritten: number;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
lastMessageAt: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* FreesailStream manages an SSE connection with a client.
|
|
30
|
+
* It handles message serialization, backpressure, and keep-alive.
|
|
31
|
+
*/
|
|
32
|
+
export class FreesailStream {
|
|
33
|
+
private res: ServerResponse;
|
|
34
|
+
private sessionId: string;
|
|
35
|
+
private keepAliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
36
|
+
private debug: boolean;
|
|
37
|
+
private _closed: boolean = false;
|
|
38
|
+
private _stats: StreamStats;
|
|
39
|
+
|
|
40
|
+
constructor(options: StreamOptions) {
|
|
41
|
+
this.res = options.response;
|
|
42
|
+
this.sessionId = options.sessionId ?? this.generateId();
|
|
43
|
+
this.debug = options.debug ?? false;
|
|
44
|
+
|
|
45
|
+
this._stats = {
|
|
46
|
+
messagesSent: 0,
|
|
47
|
+
bytesWritten: 0,
|
|
48
|
+
createdAt: Date.now(),
|
|
49
|
+
lastMessageAt: null,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Initialize SSE headers
|
|
53
|
+
this.initializeSSE();
|
|
54
|
+
|
|
55
|
+
// Start keep-alive
|
|
56
|
+
if (options.keepAliveInterval !== 0) {
|
|
57
|
+
this.startKeepAlive(options.keepAliveInterval ?? 15000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle connection close
|
|
61
|
+
this.res.on('close', () => {
|
|
62
|
+
this.close();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send an A2UX message to the client
|
|
68
|
+
*/
|
|
69
|
+
send(message: ServerToClientMessage): boolean {
|
|
70
|
+
if (this._closed) {
|
|
71
|
+
this.log('Cannot send to closed stream');
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.stringify(message);
|
|
77
|
+
const payload = `data: ${data}\n\n`;
|
|
78
|
+
|
|
79
|
+
this.res.write(payload);
|
|
80
|
+
|
|
81
|
+
this._stats.messagesSent++;
|
|
82
|
+
this._stats.bytesWritten += payload.length;
|
|
83
|
+
this._stats.lastMessageAt = Date.now();
|
|
84
|
+
|
|
85
|
+
this.log(`Sent: ${Object.keys(message)[0]}`);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.log(`Send error: ${error}`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send a createSurface message
|
|
95
|
+
*/
|
|
96
|
+
createSurface(surfaceId: string, catalogId: string): boolean {
|
|
97
|
+
return this.send({
|
|
98
|
+
createSurface: { surfaceId, catalogId }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Send an updateComponents message
|
|
104
|
+
*/
|
|
105
|
+
updateComponents(
|
|
106
|
+
surfaceId: string,
|
|
107
|
+
components: ServerToClientMessage extends { updateComponents: infer U }
|
|
108
|
+
? U extends { components: infer C } ? C : never
|
|
109
|
+
: never
|
|
110
|
+
): boolean {
|
|
111
|
+
return this.send({
|
|
112
|
+
updateComponents: { surfaceId, components }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send an updateDataModel message
|
|
118
|
+
*/
|
|
119
|
+
updateDataModel(
|
|
120
|
+
surfaceId: string,
|
|
121
|
+
value: unknown,
|
|
122
|
+
path?: string,
|
|
123
|
+
op?: 'add' | 'replace' | 'remove'
|
|
124
|
+
): boolean {
|
|
125
|
+
return this.send({
|
|
126
|
+
updateDataModel: {
|
|
127
|
+
surfaceId,
|
|
128
|
+
path: path ?? '/',
|
|
129
|
+
op: op ?? 'replace',
|
|
130
|
+
value
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Send a deleteSurface message
|
|
137
|
+
*/
|
|
138
|
+
deleteSurface(surfaceId: string): boolean {
|
|
139
|
+
return this.send({
|
|
140
|
+
deleteSurface: { surfaceId }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send a watchSurface message
|
|
146
|
+
*/
|
|
147
|
+
watchSurface(surfaceId: string, interval?: number, expiresIn?: number): boolean {
|
|
148
|
+
return this.send({
|
|
149
|
+
watchSurface: { surfaceId, interval, expiresIn }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Send an unwatchSurface message
|
|
155
|
+
*/
|
|
156
|
+
unwatchSurface(surfaceId: string): boolean {
|
|
157
|
+
return this.send({
|
|
158
|
+
unwatchSurface: { surfaceId }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Send raw data (for custom events)
|
|
164
|
+
*/
|
|
165
|
+
sendRaw(event: string, data: unknown): boolean {
|
|
166
|
+
if (this._closed) return false;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
170
|
+
this.res.write(payload);
|
|
171
|
+
this._stats.bytesWritten += payload.length;
|
|
172
|
+
return true;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Send done signal and close stream
|
|
180
|
+
*/
|
|
181
|
+
done(): void {
|
|
182
|
+
if (this._closed) return;
|
|
183
|
+
|
|
184
|
+
this.res.write('data: [DONE]\n\n');
|
|
185
|
+
this.close();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Close the stream
|
|
190
|
+
*/
|
|
191
|
+
close(): void {
|
|
192
|
+
if (this._closed) return;
|
|
193
|
+
|
|
194
|
+
this._closed = true;
|
|
195
|
+
this.stopKeepAlive();
|
|
196
|
+
|
|
197
|
+
if (!this.res.writableEnded) {
|
|
198
|
+
this.res.end();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.log('Stream closed');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if stream is closed
|
|
206
|
+
*/
|
|
207
|
+
get closed(): boolean {
|
|
208
|
+
return this._closed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get session ID
|
|
213
|
+
*/
|
|
214
|
+
get id(): string {
|
|
215
|
+
return this.sessionId;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get stream statistics
|
|
220
|
+
*/
|
|
221
|
+
get stats(): StreamStats {
|
|
222
|
+
return { ...this._stats };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
// Private Methods
|
|
227
|
+
// ===========================================================================
|
|
228
|
+
|
|
229
|
+
private initializeSSE(): void {
|
|
230
|
+
this.res.setHeader('Content-Type', 'text/event-stream');
|
|
231
|
+
this.res.setHeader('Cache-Control', 'no-cache');
|
|
232
|
+
this.res.setHeader('Connection', 'keep-alive');
|
|
233
|
+
this.res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
234
|
+
this.res.flushHeaders();
|
|
235
|
+
|
|
236
|
+
this.log('SSE initialized');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private startKeepAlive(interval: number): void {
|
|
240
|
+
this.keepAliveTimer = setInterval(() => {
|
|
241
|
+
if (!this._closed) {
|
|
242
|
+
this.res.write(': keep-alive\n\n');
|
|
243
|
+
}
|
|
244
|
+
}, interval);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private stopKeepAlive(): void {
|
|
248
|
+
if (this.keepAliveTimer) {
|
|
249
|
+
clearInterval(this.keepAliveTimer);
|
|
250
|
+
this.keepAliveTimer = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private generateId(): string {
|
|
255
|
+
return `fs_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private log(message: string): void {
|
|
259
|
+
if (this.debug) {
|
|
260
|
+
console.log(`[FreesailStream:${this.sessionId}] ${message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// Stream Store (for managing multiple connections)
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* StreamStore manages multiple SSE connections
|
|
271
|
+
*/
|
|
272
|
+
export class StreamStore {
|
|
273
|
+
private streams: Map<string, FreesailStream> = new Map();
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create and register a new stream
|
|
277
|
+
*/
|
|
278
|
+
create(options: StreamOptions): FreesailStream {
|
|
279
|
+
const stream = new FreesailStream(options);
|
|
280
|
+
this.streams.set(stream.id, stream);
|
|
281
|
+
|
|
282
|
+
// Auto-remove on close
|
|
283
|
+
options.response.on('close', () => {
|
|
284
|
+
this.streams.delete(stream.id);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return stream;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get a stream by ID
|
|
292
|
+
*/
|
|
293
|
+
get(streamId: string): FreesailStream | undefined {
|
|
294
|
+
return this.streams.get(streamId);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get all active streams
|
|
299
|
+
*/
|
|
300
|
+
getAll(): FreesailStream[] {
|
|
301
|
+
return Array.from(this.streams.values());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get stream count
|
|
306
|
+
*/
|
|
307
|
+
get count(): number {
|
|
308
|
+
return this.streams.size;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Broadcast message to all streams
|
|
313
|
+
*/
|
|
314
|
+
broadcast(message: ServerToClientMessage): void {
|
|
315
|
+
for (const stream of this.streams.values()) {
|
|
316
|
+
stream.send(message);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Close all streams
|
|
322
|
+
*/
|
|
323
|
+
closeAll(): void {
|
|
324
|
+
for (const stream of this.streams.values()) {
|
|
325
|
+
stream.close();
|
|
326
|
+
}
|
|
327
|
+
this.streams.clear();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"noImplicitAny": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"experimentalDecorators": true,
|
|
21
|
+
"useDefineForClassFields": false
|
|
22
|
+
}
|
|
23
|
+
}
|
package/index.js
DELETED