create-frootjs 1.0.9 → 1.0.10
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/templates/minimal.js +54 -54
- package/dist/templates/modular.js +261 -261
- package/dist/templates/mvc.js +196 -196
- package/dist/templates/openapi.js +168 -168
- package/dist/templates/streaming.js +176 -176
- package/dist/templates/websocket.js +163 -163
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ export const streamingTemplate = {
|
|
|
19
19
|
start: 'node dist/server.js',
|
|
20
20
|
},
|
|
21
21
|
dependencies: {
|
|
22
|
-
frootjs: '^1.0.
|
|
22
|
+
frootjs: '^1.0.5',
|
|
23
23
|
},
|
|
24
24
|
devDependencies: {
|
|
25
25
|
'@types/node': '^22.10.0',
|
|
@@ -29,182 +29,182 @@ export const streamingTemplate = {
|
|
|
29
29
|
}, null, 2) + '\n',
|
|
30
30
|
'tsconfig.json': COMMON_TSCONFIG,
|
|
31
31
|
'.gitignore': COMMON_GITIGNORE,
|
|
32
|
-
'README.md': `# ${projectName}
|
|
33
|
-
|
|
34
|
-
Froot application built with the **Real-Time Streaming & Server-Sent Events (SSE)** pattern.
|
|
35
|
-
|
|
36
|
-
## Features
|
|
37
|
-
|
|
38
|
-
- **SSE Telemetry Channel**: Streams real-time metrics (CPU memory, active requests) using \`streamSSE\`
|
|
39
|
-
- **AI Token Streamer**: Demonstrates streaming LLM-style markdown tokens chunk-by-chunk using \`streamText\`
|
|
40
|
-
- **Live Interactive Dashboard**: Embedded web client displaying real-time graphs and live chat stream
|
|
41
|
-
|
|
42
|
-
## Getting Started
|
|
43
|
-
|
|
44
|
-
\`\`\`bash
|
|
45
|
-
# 1. Install dependencies
|
|
46
|
-
pnpm install
|
|
47
|
-
|
|
48
|
-
# 2. Run in development mode
|
|
49
|
-
pnpm run dev
|
|
50
|
-
|
|
51
|
-
# 3. Open in browser
|
|
52
|
-
http://localhost:3000
|
|
53
|
-
\`\`\`
|
|
32
|
+
'README.md': `# ${projectName}
|
|
33
|
+
|
|
34
|
+
Froot application built with the **Real-Time Streaming & Server-Sent Events (SSE)** pattern.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **SSE Telemetry Channel**: Streams real-time metrics (CPU memory, active requests) using \`streamSSE\`
|
|
39
|
+
- **AI Token Streamer**: Demonstrates streaming LLM-style markdown tokens chunk-by-chunk using \`streamText\`
|
|
40
|
+
- **Live Interactive Dashboard**: Embedded web client displaying real-time graphs and live chat stream
|
|
41
|
+
|
|
42
|
+
## Getting Started
|
|
43
|
+
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
# 1. Install dependencies
|
|
46
|
+
pnpm install
|
|
47
|
+
|
|
48
|
+
# 2. Run in development mode
|
|
49
|
+
pnpm run dev
|
|
50
|
+
|
|
51
|
+
# 3. Open in browser
|
|
52
|
+
http://localhost:3000
|
|
53
|
+
\`\`\`
|
|
54
54
|
`,
|
|
55
|
-
'src/server.ts': `import { Froot } from 'frootjs';
|
|
56
|
-
import { cors } from 'frootjs/cors';
|
|
57
|
-
import { streamSSE, streamText, sleep } from 'frootjs/streaming';
|
|
58
|
-
|
|
59
|
-
export const app = new Froot();
|
|
60
|
-
|
|
61
|
-
app.use(cors());
|
|
62
|
-
|
|
63
|
-
// Embedded Dashboard HTML
|
|
64
|
-
app.get('/', (c) => {
|
|
65
|
-
return c.html(\`<!DOCTYPE html>
|
|
66
|
-
<html lang="en">
|
|
67
|
-
<head>
|
|
68
|
-
<meta charset="UTF-8">
|
|
69
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
-
<title>Froot Streaming Demo</title>
|
|
71
|
-
<style>
|
|
72
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
73
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
|
|
74
|
-
.container { max-width: 800px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem; }
|
|
75
|
-
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.5rem; }
|
|
76
|
-
h1 { color: #10b981; font-size: 1.75rem; margin-bottom: 0.5rem; }
|
|
77
|
-
h2 { font-size: 1.25rem; margin-bottom: 1rem; color: #38bdf8; }
|
|
78
|
-
pre { background: #0b0f19; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.9rem; line-height: 1.4; }
|
|
79
|
-
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem; }
|
|
80
|
-
.metric-box { background: #0b0f19; padding: 1rem; border-radius: 6px; text-align: center; }
|
|
81
|
-
.metric-value { font-size: 1.5rem; font-weight: bold; color: #10b981; }
|
|
82
|
-
.metric-label { font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; margin-top: 0.25rem; }
|
|
83
|
-
button { background: #10b981; color: #0f172a; border: none; padding: 0.6rem 1.2rem; border-radius: 6px; font-weight: bold; cursor: pointer; }
|
|
84
|
-
button:hover { background: #059669; }
|
|
85
|
-
</style>
|
|
86
|
-
</head>
|
|
87
|
-
<body>
|
|
88
|
-
<div class="container">
|
|
89
|
-
<div>
|
|
90
|
-
<h1>Froot Streaming Engine</h1>
|
|
91
|
-
<p style="color: #94a3b8;">High-throughput Server-Sent Events & chunked HTTP streaming</p>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
<!-- SSE Metrics Card -->
|
|
95
|
-
<div class="card">
|
|
96
|
-
<h2>Live Telemetry Stream (SSE)</h2>
|
|
97
|
-
<div class="metrics">
|
|
98
|
-
<div class="metric-box">
|
|
99
|
-
<div id="m-memory" class="metric-value">--</div>
|
|
100
|
-
<div class="metric-label">Memory (MB)</div>
|
|
101
|
-
</div>
|
|
102
|
-
<div class="metric-box">
|
|
103
|
-
<div id="m-uptime" class="metric-value">--</div>
|
|
104
|
-
<div class="metric-label">Uptime (s)</div>
|
|
105
|
-
</div>
|
|
106
|
-
<div class="metric-box">
|
|
107
|
-
<div id="m-count" class="metric-value">0</div>
|
|
108
|
-
<div class="metric-label">Events Received</div>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
<pre id="sse-log">Connecting to /sse/metrics...</pre>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<!-- LLM Stream Card -->
|
|
115
|
-
<div class="card">
|
|
116
|
-
<h2>AI Token Stream Simulation</h2>
|
|
117
|
-
<div style="margin-bottom: 1rem;">
|
|
118
|
-
<button id="btn-stream">Generate Streaming Text</button>
|
|
119
|
-
</div>
|
|
120
|
-
<pre id="stream-output">Click button to trigger token-by-token stream...</pre>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<script>
|
|
125
|
-
// SSE Stream
|
|
126
|
-
let count = 0;
|
|
127
|
-
const es = new EventSource('/sse/metrics');
|
|
128
|
-
const log = document.getElementById('sse-log');
|
|
129
|
-
es.addEventListener('telemetry', (e) => {
|
|
130
|
-
const data = JSON.parse(e.data);
|
|
131
|
-
count++;
|
|
132
|
-
document.getElementById('m-memory').textContent = data.memoryUsageMB;
|
|
133
|
-
document.getElementById('m-uptime').textContent = data.uptimeSec;
|
|
134
|
-
document.getElementById('m-count').textContent = count;
|
|
135
|
-
log.textContent = 'Last event: ' + JSON.stringify(data, null, 2);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Token Stream
|
|
139
|
-
const btn = document.getElementById('btn-stream');
|
|
140
|
-
const output = document.getElementById('stream-output');
|
|
141
|
-
btn.addEventListener('click', async () => {
|
|
142
|
-
btn.disabled = true;
|
|
143
|
-
output.textContent = '';
|
|
144
|
-
const response = await fetch('/api/chat/stream');
|
|
145
|
-
const reader = response.body.getReader();
|
|
146
|
-
const decoder = new TextDecoder();
|
|
147
|
-
while (true) {
|
|
148
|
-
const { value, done } = await reader.read();
|
|
149
|
-
if (done) break;
|
|
150
|
-
output.textContent += decoder.decode(value);
|
|
151
|
-
}
|
|
152
|
-
btn.disabled = false;
|
|
153
|
-
});
|
|
154
|
-
</script>
|
|
155
|
-
</body>
|
|
156
|
-
</html>\`);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Real-time SSE Telemetry endpoint
|
|
160
|
-
app.get('/sse/metrics', (c) => {
|
|
161
|
-
return streamSSE(
|
|
162
|
-
c,
|
|
163
|
-
async (stream) => {
|
|
164
|
-
let eventId = 1;
|
|
165
|
-
while (!stream.signal.aborted) {
|
|
166
|
-
const mem = process.memoryUsage();
|
|
167
|
-
const payload = {
|
|
168
|
-
memoryUsageMB: Math.round(mem.rss / 1024 / 1024),
|
|
169
|
-
uptimeSec: Math.floor(process.uptime()),
|
|
170
|
-
timestamp: new Date().toISOString(),
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
await stream.writeSSE({
|
|
174
|
-
event: 'telemetry',
|
|
175
|
-
id: String(eventId++),
|
|
176
|
-
data: JSON.stringify(payload),
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await sleep(1000, stream.signal);
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
{ heartbeatMs: 15_000 }
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// AI Token Streaming Simulation endpoint
|
|
187
|
-
app.get('/api/chat/stream', (c) => {
|
|
188
|
-
const sampleResponse = [
|
|
189
|
-
'Hello! ', 'I ', 'am ', 'streaming ', 'from ', 'a ', 'Froot ', 'server. ',
|
|
190
|
-
'Each ', 'token ', 'is ', 'dispatched ', 'with ', 'zero-copy ', 'efficiency.\\n\\n',
|
|
191
|
-
'Froot ', 'supports ', 'standards-compliant ', 'WHATWG ', 'ReadableStream ',
|
|
192
|
-
'pipelines ', 'compatible ', 'with ', 'all ', 'modern ', 'edge ', 'runtimes.',
|
|
193
|
-
];
|
|
194
|
-
|
|
195
|
-
return streamText(c, async (stream) => {
|
|
196
|
-
for (const chunk of sampleResponse) {
|
|
197
|
-
if (stream.signal.aborted) break;
|
|
198
|
-
await stream.write(chunk);
|
|
199
|
-
await sleep(60, stream.signal);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const PORT = Number(process.env.PORT) || 3000;
|
|
205
|
-
app.listen(PORT, () => {
|
|
206
|
-
console.log(\`Streaming server running on http://localhost:\${PORT}\`);
|
|
207
|
-
});
|
|
55
|
+
'src/server.ts': `import { Froot } from 'frootjs';
|
|
56
|
+
import { cors } from 'frootjs/cors';
|
|
57
|
+
import { streamSSE, streamText, sleep } from 'frootjs/streaming';
|
|
58
|
+
|
|
59
|
+
export const app = new Froot();
|
|
60
|
+
|
|
61
|
+
app.use(cors());
|
|
62
|
+
|
|
63
|
+
// Embedded Dashboard HTML
|
|
64
|
+
app.get('/', (c) => {
|
|
65
|
+
return c.html(\`<!DOCTYPE html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="UTF-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
+
<title>Froot Streaming Demo</title>
|
|
71
|
+
<style>
|
|
72
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
73
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
|
|
74
|
+
.container { max-width: 800px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem; }
|
|
75
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.5rem; }
|
|
76
|
+
h1 { color: #10b981; font-size: 1.75rem; margin-bottom: 0.5rem; }
|
|
77
|
+
h2 { font-size: 1.25rem; margin-bottom: 1rem; color: #38bdf8; }
|
|
78
|
+
pre { background: #0b0f19; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.9rem; line-height: 1.4; }
|
|
79
|
+
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem; }
|
|
80
|
+
.metric-box { background: #0b0f19; padding: 1rem; border-radius: 6px; text-align: center; }
|
|
81
|
+
.metric-value { font-size: 1.5rem; font-weight: bold; color: #10b981; }
|
|
82
|
+
.metric-label { font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; margin-top: 0.25rem; }
|
|
83
|
+
button { background: #10b981; color: #0f172a; border: none; padding: 0.6rem 1.2rem; border-radius: 6px; font-weight: bold; cursor: pointer; }
|
|
84
|
+
button:hover { background: #059669; }
|
|
85
|
+
</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<div class="container">
|
|
89
|
+
<div>
|
|
90
|
+
<h1>Froot Streaming Engine</h1>
|
|
91
|
+
<p style="color: #94a3b8;">High-throughput Server-Sent Events & chunked HTTP streaming</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- SSE Metrics Card -->
|
|
95
|
+
<div class="card">
|
|
96
|
+
<h2>Live Telemetry Stream (SSE)</h2>
|
|
97
|
+
<div class="metrics">
|
|
98
|
+
<div class="metric-box">
|
|
99
|
+
<div id="m-memory" class="metric-value">--</div>
|
|
100
|
+
<div class="metric-label">Memory (MB)</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="metric-box">
|
|
103
|
+
<div id="m-uptime" class="metric-value">--</div>
|
|
104
|
+
<div class="metric-label">Uptime (s)</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="metric-box">
|
|
107
|
+
<div id="m-count" class="metric-value">0</div>
|
|
108
|
+
<div class="metric-label">Events Received</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<pre id="sse-log">Connecting to /sse/metrics...</pre>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- LLM Stream Card -->
|
|
115
|
+
<div class="card">
|
|
116
|
+
<h2>AI Token Stream Simulation</h2>
|
|
117
|
+
<div style="margin-bottom: 1rem;">
|
|
118
|
+
<button id="btn-stream">Generate Streaming Text</button>
|
|
119
|
+
</div>
|
|
120
|
+
<pre id="stream-output">Click button to trigger token-by-token stream...</pre>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
// SSE Stream
|
|
126
|
+
let count = 0;
|
|
127
|
+
const es = new EventSource('/sse/metrics');
|
|
128
|
+
const log = document.getElementById('sse-log');
|
|
129
|
+
es.addEventListener('telemetry', (e) => {
|
|
130
|
+
const data = JSON.parse(e.data);
|
|
131
|
+
count++;
|
|
132
|
+
document.getElementById('m-memory').textContent = data.memoryUsageMB;
|
|
133
|
+
document.getElementById('m-uptime').textContent = data.uptimeSec;
|
|
134
|
+
document.getElementById('m-count').textContent = count;
|
|
135
|
+
log.textContent = 'Last event: ' + JSON.stringify(data, null, 2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Token Stream
|
|
139
|
+
const btn = document.getElementById('btn-stream');
|
|
140
|
+
const output = document.getElementById('stream-output');
|
|
141
|
+
btn.addEventListener('click', async () => {
|
|
142
|
+
btn.disabled = true;
|
|
143
|
+
output.textContent = '';
|
|
144
|
+
const response = await fetch('/api/chat/stream');
|
|
145
|
+
const reader = response.body.getReader();
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
while (true) {
|
|
148
|
+
const { value, done } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
output.textContent += decoder.decode(value);
|
|
151
|
+
}
|
|
152
|
+
btn.disabled = false;
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
155
|
+
</body>
|
|
156
|
+
</html>\`);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Real-time SSE Telemetry endpoint
|
|
160
|
+
app.get('/sse/metrics', (c) => {
|
|
161
|
+
return streamSSE(
|
|
162
|
+
c,
|
|
163
|
+
async (stream) => {
|
|
164
|
+
let eventId = 1;
|
|
165
|
+
while (!stream.signal.aborted) {
|
|
166
|
+
const mem = process.memoryUsage();
|
|
167
|
+
const payload = {
|
|
168
|
+
memoryUsageMB: Math.round(mem.rss / 1024 / 1024),
|
|
169
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
170
|
+
timestamp: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
await stream.writeSSE({
|
|
174
|
+
event: 'telemetry',
|
|
175
|
+
id: String(eventId++),
|
|
176
|
+
data: JSON.stringify(payload),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await sleep(1000, stream.signal);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{ heartbeatMs: 15_000 }
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// AI Token Streaming Simulation endpoint
|
|
187
|
+
app.get('/api/chat/stream', (c) => {
|
|
188
|
+
const sampleResponse = [
|
|
189
|
+
'Hello! ', 'I ', 'am ', 'streaming ', 'from ', 'a ', 'Froot ', 'server. ',
|
|
190
|
+
'Each ', 'token ', 'is ', 'dispatched ', 'with ', 'zero-copy ', 'efficiency.\\n\\n',
|
|
191
|
+
'Froot ', 'supports ', 'standards-compliant ', 'WHATWG ', 'ReadableStream ',
|
|
192
|
+
'pipelines ', 'compatible ', 'with ', 'all ', 'modern ', 'edge ', 'runtimes.',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
return streamText(c, async (stream) => {
|
|
196
|
+
for (const chunk of sampleResponse) {
|
|
197
|
+
if (stream.signal.aborted) break;
|
|
198
|
+
await stream.write(chunk);
|
|
199
|
+
await sleep(60, stream.signal);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
205
|
+
app.listen(PORT, () => {
|
|
206
|
+
console.log(\`Streaming server running on http://localhost:\${PORT}\`);
|
|
207
|
+
});
|
|
208
208
|
`,
|
|
209
209
|
};
|
|
210
210
|
},
|