@flink-app/streaming-plugin 0.12.1-alpha.45
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/LICENSE +21 -0
- package/README.md +476 -0
- package/dist/StreamingPlugin.d.ts +45 -0
- package/dist/StreamingPlugin.d.ts.map +1 -0
- package/dist/StreamingPlugin.js +276 -0
- package/dist/StreamingPlugin.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/examples/authenticated-stream.ts +88 -0
- package/examples/client-ndjson.html +157 -0
- package/examples/client-sse.html +202 -0
- package/package.json +41 -0
- package/spec/StreamingPlugin.spec.ts +513 -0
- package/spec/support/jasmine.json +7 -0
- package/src/StreamingPlugin.ts +281 -0
- package/src/index.ts +13 -0
- package/src/types.ts +101 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.StreamingPlugin = void 0;
|
|
40
|
+
exports.streamingPlugin = streamingPlugin;
|
|
41
|
+
var flink_1 = require("@flink-app/flink");
|
|
42
|
+
/**
|
|
43
|
+
* Creates a stream writer for the given format
|
|
44
|
+
*/
|
|
45
|
+
function createStreamWriter(res, format, debug) {
|
|
46
|
+
var closed = false;
|
|
47
|
+
res.on("close", function () {
|
|
48
|
+
closed = true;
|
|
49
|
+
if (debug) {
|
|
50
|
+
flink_1.log.debug("[StreamingPlugin] Client closed connection");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
write: function (data) {
|
|
55
|
+
if (closed) {
|
|
56
|
+
if (debug) {
|
|
57
|
+
flink_1.log.warn("[StreamingPlugin] Attempted write to closed stream");
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (format === "sse") {
|
|
63
|
+
// SSE format: data: {json}\n\n
|
|
64
|
+
res.write("data: ".concat(JSON.stringify(data), "\n\n"));
|
|
65
|
+
}
|
|
66
|
+
else if (format === "ndjson") {
|
|
67
|
+
// NDJSON format: {json}\n
|
|
68
|
+
res.write("".concat(JSON.stringify(data), "\n"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
flink_1.log.error("[StreamingPlugin] Error writing to stream:", err);
|
|
73
|
+
closed = true;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
error: function (error) {
|
|
77
|
+
if (closed)
|
|
78
|
+
return;
|
|
79
|
+
var errorMessage = typeof error === "string" ? error : error.message;
|
|
80
|
+
try {
|
|
81
|
+
if (format === "sse") {
|
|
82
|
+
// SSE error event
|
|
83
|
+
res.write("event: error\ndata: ".concat(JSON.stringify({ message: errorMessage }), "\n\n"));
|
|
84
|
+
}
|
|
85
|
+
else if (format === "ndjson") {
|
|
86
|
+
// NDJSON error object
|
|
87
|
+
res.write("".concat(JSON.stringify({ error: errorMessage }), "\n"));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
flink_1.log.error("[StreamingPlugin] Error writing error to stream:", err);
|
|
92
|
+
closed = true;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
end: function () {
|
|
96
|
+
if (closed)
|
|
97
|
+
return;
|
|
98
|
+
closed = true;
|
|
99
|
+
res.end();
|
|
100
|
+
},
|
|
101
|
+
isOpen: function () {
|
|
102
|
+
return !closed;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Streaming plugin for Flink Framework
|
|
108
|
+
* Provides SSE and NDJSON streaming support
|
|
109
|
+
*/
|
|
110
|
+
var StreamingPlugin = /** @class */ (function () {
|
|
111
|
+
function StreamingPlugin(options) {
|
|
112
|
+
if (options === void 0) { options = {}; }
|
|
113
|
+
this.id = "streaming";
|
|
114
|
+
this.options = {
|
|
115
|
+
defaultFormat: options.defaultFormat || "sse",
|
|
116
|
+
debug: options.debug || false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
StreamingPlugin.prototype.init = function (app) {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
121
|
+
return __generator(this, function (_a) {
|
|
122
|
+
this.app = app;
|
|
123
|
+
if (this.options.debug) {
|
|
124
|
+
flink_1.log.info("[StreamingPlugin] Initialized with default format: ".concat(this.options.defaultFormat));
|
|
125
|
+
}
|
|
126
|
+
return [2 /*return*/];
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
// Implementation
|
|
131
|
+
StreamingPlugin.prototype.registerStreamHandler = function (handlerOrModule, routeProps) {
|
|
132
|
+
var _this = this;
|
|
133
|
+
if (!this.app) {
|
|
134
|
+
throw new Error("[StreamingPlugin] Plugin not initialized - call app.start() first");
|
|
135
|
+
}
|
|
136
|
+
if (!this.app.expressApp) {
|
|
137
|
+
throw new Error("[StreamingPlugin] Express app not available");
|
|
138
|
+
}
|
|
139
|
+
// Determine if we received a module or explicit handler+props
|
|
140
|
+
var handler;
|
|
141
|
+
var props;
|
|
142
|
+
if (typeof handlerOrModule === "function") {
|
|
143
|
+
// Explicit handler function + route props
|
|
144
|
+
if (!routeProps) {
|
|
145
|
+
throw new Error("[StreamingPlugin] Route props are required when registering with explicit handler function");
|
|
146
|
+
}
|
|
147
|
+
handler = handlerOrModule;
|
|
148
|
+
props = routeProps;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Handler module with default export and Route
|
|
152
|
+
var module_1 = handlerOrModule;
|
|
153
|
+
if (!module_1.default) {
|
|
154
|
+
throw new Error("[StreamingPlugin] Handler module must have a default export");
|
|
155
|
+
}
|
|
156
|
+
if (!module_1.Route) {
|
|
157
|
+
throw new Error("[StreamingPlugin] Handler module must export Route configuration");
|
|
158
|
+
}
|
|
159
|
+
handler = module_1.default;
|
|
160
|
+
props = module_1.Route;
|
|
161
|
+
}
|
|
162
|
+
var method = props.method || flink_1.HttpMethod.get;
|
|
163
|
+
var format = props.format || this.options.defaultFormat;
|
|
164
|
+
var methodAndRoute = "".concat(method.toUpperCase(), " ").concat(props.path);
|
|
165
|
+
// Register Express route
|
|
166
|
+
this.app.expressApp[method](props.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
167
|
+
var authenticated, err_1, streamWriter, err_2;
|
|
168
|
+
return __generator(this, function (_a) {
|
|
169
|
+
switch (_a.label) {
|
|
170
|
+
case 0:
|
|
171
|
+
if (!props.permissions) return [3 /*break*/, 4];
|
|
172
|
+
if (!this.app.auth) {
|
|
173
|
+
flink_1.log.error("[StreamingPlugin] ".concat(methodAndRoute, " requires authentication but no auth plugin is configured"));
|
|
174
|
+
res.status(500).json({
|
|
175
|
+
status: 500,
|
|
176
|
+
error: {
|
|
177
|
+
title: "Internal Server Error",
|
|
178
|
+
detail: "Authentication not configured",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return [2 /*return*/];
|
|
182
|
+
}
|
|
183
|
+
_a.label = 1;
|
|
184
|
+
case 1:
|
|
185
|
+
_a.trys.push([1, 3, , 4]);
|
|
186
|
+
return [4 /*yield*/, this.app.auth.authenticateRequest(req, props.permissions)];
|
|
187
|
+
case 2:
|
|
188
|
+
authenticated = _a.sent();
|
|
189
|
+
if (!authenticated) {
|
|
190
|
+
res.status(401).json({
|
|
191
|
+
status: 401,
|
|
192
|
+
error: {
|
|
193
|
+
title: "Unauthorized",
|
|
194
|
+
detail: "Authentication required",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return [2 /*return*/];
|
|
198
|
+
}
|
|
199
|
+
return [3 /*break*/, 4];
|
|
200
|
+
case 3:
|
|
201
|
+
err_1 = _a.sent();
|
|
202
|
+
flink_1.log.error("[StreamingPlugin] ".concat(methodAndRoute, " authentication error:"), err_1);
|
|
203
|
+
res.status(401).json({
|
|
204
|
+
status: 401,
|
|
205
|
+
error: {
|
|
206
|
+
title: "Unauthorized",
|
|
207
|
+
detail: "Authentication failed",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
return [2 /*return*/];
|
|
211
|
+
case 4:
|
|
212
|
+
// Set headers based on format
|
|
213
|
+
if (format === "sse") {
|
|
214
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
215
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
216
|
+
res.setHeader("Connection", "keep-alive");
|
|
217
|
+
res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
|
|
218
|
+
}
|
|
219
|
+
else if (format === "ndjson") {
|
|
220
|
+
res.setHeader("Content-Type", "application/x-ndjson");
|
|
221
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
222
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
223
|
+
}
|
|
224
|
+
// Flush headers immediately
|
|
225
|
+
res.flushHeaders();
|
|
226
|
+
streamWriter = createStreamWriter(res, format, this.options.debug);
|
|
227
|
+
_a.label = 5;
|
|
228
|
+
case 5:
|
|
229
|
+
_a.trys.push([5, 7, 8, 9]);
|
|
230
|
+
if (this.options.debug) {
|
|
231
|
+
flink_1.log.debug("[StreamingPlugin] ".concat(methodAndRoute, " - Stream started"));
|
|
232
|
+
}
|
|
233
|
+
// Call the handler
|
|
234
|
+
return [4 /*yield*/, handler({
|
|
235
|
+
req: req,
|
|
236
|
+
ctx: this.app.ctx,
|
|
237
|
+
stream: streamWriter,
|
|
238
|
+
origin: props.origin,
|
|
239
|
+
})];
|
|
240
|
+
case 6:
|
|
241
|
+
// Call the handler
|
|
242
|
+
_a.sent();
|
|
243
|
+
if (this.options.debug) {
|
|
244
|
+
flink_1.log.debug("[StreamingPlugin] ".concat(methodAndRoute, " - Handler completed"));
|
|
245
|
+
}
|
|
246
|
+
return [3 /*break*/, 9];
|
|
247
|
+
case 7:
|
|
248
|
+
err_2 = _a.sent();
|
|
249
|
+
flink_1.log.error("[StreamingPlugin] ".concat(methodAndRoute, " - Handler error:"), err_2);
|
|
250
|
+
// Send error to client if stream still open
|
|
251
|
+
if (streamWriter.isOpen()) {
|
|
252
|
+
streamWriter.error(err_2);
|
|
253
|
+
}
|
|
254
|
+
return [3 /*break*/, 9];
|
|
255
|
+
case 8:
|
|
256
|
+
// Ensure stream is closed
|
|
257
|
+
if (streamWriter.isOpen()) {
|
|
258
|
+
streamWriter.end();
|
|
259
|
+
}
|
|
260
|
+
return [7 /*endfinally*/];
|
|
261
|
+
case 9: return [2 /*return*/];
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}); });
|
|
265
|
+
flink_1.log.info("[StreamingPlugin] Registered streaming route ".concat(methodAndRoute, " (").concat(format, ")"));
|
|
266
|
+
};
|
|
267
|
+
return StreamingPlugin;
|
|
268
|
+
}());
|
|
269
|
+
exports.StreamingPlugin = StreamingPlugin;
|
|
270
|
+
/**
|
|
271
|
+
* Factory function to create streaming plugin
|
|
272
|
+
*/
|
|
273
|
+
function streamingPlugin(options) {
|
|
274
|
+
return new StreamingPlugin(options);
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=StreamingPlugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StreamingPlugin.js","sourceRoot":"","sources":["../src/StreamingPlugin.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGrE,YAAY,EACR,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,GACzB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.streamingPlugin = exports.StreamingPlugin = void 0;
|
|
4
|
+
// Main plugin export
|
|
5
|
+
var StreamingPlugin_1 = require("./StreamingPlugin");
|
|
6
|
+
Object.defineProperty(exports, "StreamingPlugin", { enumerable: true, get: function () { return StreamingPlugin_1.StreamingPlugin; } });
|
|
7
|
+
Object.defineProperty(exports, "streamingPlugin", { enumerable: true, get: function () { return StreamingPlugin_1.streamingPlugin; } });
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qBAAqB;AACrB,qDAAqE;AAA5D,kHAAA,eAAe,OAAA;AAAE,kHAAA,eAAe,OAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { FlinkContext, FlinkRequest, RouteProps, HttpMethod } from "@flink-app/flink";
|
|
2
|
+
/**
|
|
3
|
+
* Streaming format types
|
|
4
|
+
*/
|
|
5
|
+
export type StreamFormat = "sse" | "ndjson";
|
|
6
|
+
/**
|
|
7
|
+
* Stream writer interface for sending data to clients
|
|
8
|
+
*/
|
|
9
|
+
export interface StreamWriter<T = any> {
|
|
10
|
+
/**
|
|
11
|
+
* Write data to the stream
|
|
12
|
+
*/
|
|
13
|
+
write(data: T): void;
|
|
14
|
+
/**
|
|
15
|
+
* Send an error event to the stream
|
|
16
|
+
*/
|
|
17
|
+
error(error: Error | string): void;
|
|
18
|
+
/**
|
|
19
|
+
* End the stream
|
|
20
|
+
*/
|
|
21
|
+
end(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Check if the connection is still open
|
|
24
|
+
*/
|
|
25
|
+
isOpen(): boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Props passed to streaming handlers
|
|
29
|
+
*/
|
|
30
|
+
export interface StreamHandlerProps<Ctx extends FlinkContext, T = any> {
|
|
31
|
+
req: FlinkRequest;
|
|
32
|
+
ctx: Ctx;
|
|
33
|
+
stream: StreamWriter<T>;
|
|
34
|
+
origin?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Streaming handler function signature
|
|
38
|
+
*/
|
|
39
|
+
export type StreamHandler<Ctx extends FlinkContext, T = any> = (props: StreamHandlerProps<Ctx, T>) => Promise<void> | void;
|
|
40
|
+
/**
|
|
41
|
+
* Route configuration for streaming endpoints
|
|
42
|
+
*/
|
|
43
|
+
export interface StreamingRouteProps extends Omit<RouteProps, "skipAutoRegister"> {
|
|
44
|
+
/**
|
|
45
|
+
* HTTP path for the streaming endpoint
|
|
46
|
+
*/
|
|
47
|
+
path: string;
|
|
48
|
+
/**
|
|
49
|
+
* HTTP method (defaults to GET)
|
|
50
|
+
*/
|
|
51
|
+
method?: HttpMethod;
|
|
52
|
+
/**
|
|
53
|
+
* Streaming format to use
|
|
54
|
+
* - 'sse': Server-Sent Events (text/event-stream)
|
|
55
|
+
* - 'ndjson': Newline-Delimited JSON (application/x-ndjson)
|
|
56
|
+
* @default 'sse'
|
|
57
|
+
*/
|
|
58
|
+
format?: StreamFormat;
|
|
59
|
+
/**
|
|
60
|
+
* Must be true for streaming handlers (prevents auto-registration)
|
|
61
|
+
*/
|
|
62
|
+
skipAutoRegister: true;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Options for creating the streaming plugin
|
|
66
|
+
*/
|
|
67
|
+
export interface StreamingPluginOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Default format if not specified in route props
|
|
70
|
+
* @default 'sse'
|
|
71
|
+
*/
|
|
72
|
+
defaultFormat?: StreamFormat;
|
|
73
|
+
/**
|
|
74
|
+
* Enable debug logging
|
|
75
|
+
* @default false
|
|
76
|
+
*/
|
|
77
|
+
debug?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Handler module type (namespace import with default export and Route)
|
|
81
|
+
*/
|
|
82
|
+
export interface StreamHandlerModule<Ctx extends FlinkContext = any, T = any> {
|
|
83
|
+
default: StreamHandler<Ctx, T>;
|
|
84
|
+
Route: StreamingRouteProps;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEtF;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,GAAG;IACjC;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAErB;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IAEnC;;OAEG;IACH,GAAG,IAAI,IAAI,CAAC;IAEZ;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,GAAG,SAAS,YAAY,EAAE,CAAC,GAAG,GAAG;IACjE,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,CAAC,GAAG,SAAS,YAAY,EAAE,CAAC,GAAG,GAAG,IAAI,CAC3D,KAAK,EAAE,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,KAChC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC;IAC7E;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IAEpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IAEtB;;OAEG;IACH,gBAAgB,EAAE,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACnC;;;OAGG;IACH,aAAa,CAAC,EAAE,YAAY,CAAC;IAE7B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,GAAG,SAAS,YAAY,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG;IACxE,OAAO,EAAE,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAC/B,KAAK,EAAE,mBAAmB,CAAC;CAC9B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Authenticated Streaming Handler
|
|
3
|
+
*
|
|
4
|
+
* This example shows how to use authentication with streaming handlers.
|
|
5
|
+
* Works with any Flink auth plugin (JWT, BankID, OAuth, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StreamHandler, StreamingRouteProps } from "@flink-app/streaming-plugin";
|
|
9
|
+
import { FlinkContext } from "@flink-app/flink";
|
|
10
|
+
|
|
11
|
+
// Route configuration with permissions
|
|
12
|
+
export const Route: StreamingRouteProps = {
|
|
13
|
+
path: "/admin/live-metrics",
|
|
14
|
+
format: "sse",
|
|
15
|
+
skipAutoRegister: true,
|
|
16
|
+
permissions: ["admin"], // Only users with 'admin' role can access
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface MetricEvent {
|
|
20
|
+
metric: string;
|
|
21
|
+
value: number;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
userId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Authenticated streaming handler
|
|
28
|
+
* Only accessible to users with 'admin' permission
|
|
29
|
+
*/
|
|
30
|
+
const GetLiveMetrics: StreamHandler<FlinkContext, MetricEvent> = async ({ req, stream }) => {
|
|
31
|
+
// req.user is populated by the auth plugin after successful authentication
|
|
32
|
+
const userId = (req as any).user?.userId || "unknown";
|
|
33
|
+
|
|
34
|
+
console.log(`Admin user ${userId} connected to live metrics stream`);
|
|
35
|
+
|
|
36
|
+
let count = 0;
|
|
37
|
+
const maxMetrics = 20;
|
|
38
|
+
|
|
39
|
+
// Stream metrics every second
|
|
40
|
+
const interval = setInterval(() => {
|
|
41
|
+
if (!stream.isOpen() || count >= maxMetrics) {
|
|
42
|
+
clearInterval(interval);
|
|
43
|
+
stream.end();
|
|
44
|
+
console.log(`User ${userId} disconnected from metrics stream`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Send metric with user context
|
|
49
|
+
stream.write({
|
|
50
|
+
metric: "cpu_usage",
|
|
51
|
+
value: Math.random() * 100,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
userId,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
count++;
|
|
57
|
+
}, 1000);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default GetLiveMetrics;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* CLIENT USAGE:
|
|
64
|
+
*
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // Must include authentication header (depends on your auth plugin)
|
|
67
|
+
* const eventSource = new EventSource('/admin/live-metrics', {
|
|
68
|
+
* headers: {
|
|
69
|
+
* 'Authorization': 'Bearer your-jwt-token'
|
|
70
|
+
* }
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* eventSource.onmessage = (event) => {
|
|
74
|
+
* const metric = JSON.parse(event.data);
|
|
75
|
+
* console.log(`Metric from user ${metric.userId}:`, metric);
|
|
76
|
+
* };
|
|
77
|
+
*
|
|
78
|
+
* eventSource.onerror = (error) => {
|
|
79
|
+
* console.error('Authentication failed or connection lost');
|
|
80
|
+
* };
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Note: EventSource API doesn't support custom headers in browsers.
|
|
84
|
+
* For authenticated SSE, you may need to:
|
|
85
|
+
* 1. Pass token as query parameter: `/admin/live-metrics?token=...`
|
|
86
|
+
* 2. Use cookie-based authentication
|
|
87
|
+
* 3. Use Fetch API with ReadableStream instead of EventSource
|
|
88
|
+
*/
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>NDJSON Streaming Example</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
max-width: 800px;
|
|
11
|
+
margin: 50px auto;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
}
|
|
14
|
+
#output {
|
|
15
|
+
background: #f5f5f5;
|
|
16
|
+
padding: 20px;
|
|
17
|
+
border-radius: 8px;
|
|
18
|
+
min-height: 200px;
|
|
19
|
+
white-space: pre-wrap;
|
|
20
|
+
font-family: 'Monaco', monospace;
|
|
21
|
+
font-size: 14px;
|
|
22
|
+
}
|
|
23
|
+
button {
|
|
24
|
+
background: #007bff;
|
|
25
|
+
color: white;
|
|
26
|
+
border: none;
|
|
27
|
+
padding: 10px 20px;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
font-size: 16px;
|
|
31
|
+
}
|
|
32
|
+
button:hover {
|
|
33
|
+
background: #0056b3;
|
|
34
|
+
}
|
|
35
|
+
button:disabled {
|
|
36
|
+
background: #ccc;
|
|
37
|
+
cursor: not-allowed;
|
|
38
|
+
}
|
|
39
|
+
input {
|
|
40
|
+
padding: 10px;
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
border: 1px solid #ddd;
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
width: 300px;
|
|
45
|
+
}
|
|
46
|
+
.controls {
|
|
47
|
+
margin-bottom: 20px;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<h1>NDJSON Streaming Example</h1>
|
|
53
|
+
<p>This example demonstrates consuming NDJSON streams from Flink (LLM-style chat streaming).</p>
|
|
54
|
+
|
|
55
|
+
<div class="controls">
|
|
56
|
+
<input type="text" id="prompt" placeholder="Enter your prompt..." value="Tell me a story">
|
|
57
|
+
<button id="startBtn" onclick="startStream()">Start Stream</button>
|
|
58
|
+
<button id="stopBtn" onclick="stopStream()" disabled>Stop Stream</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<h3>Response:</h3>
|
|
62
|
+
<div id="output"></div>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
let reader = null;
|
|
66
|
+
let controller = null;
|
|
67
|
+
|
|
68
|
+
async function startStream() {
|
|
69
|
+
const prompt = document.getElementById('prompt').value;
|
|
70
|
+
const output = document.getElementById('output');
|
|
71
|
+
const startBtn = document.getElementById('startBtn');
|
|
72
|
+
const stopBtn = document.getElementById('stopBtn');
|
|
73
|
+
|
|
74
|
+
output.textContent = '';
|
|
75
|
+
startBtn.disabled = true;
|
|
76
|
+
stopBtn.disabled = false;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`http://localhost:3333/chat/stream?prompt=${encodeURIComponent(prompt)}`);
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
reader = response.body.getReader();
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
let buffer = '';
|
|
88
|
+
|
|
89
|
+
while (true) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
|
|
92
|
+
if (done) {
|
|
93
|
+
console.log('Stream complete');
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Decode chunk
|
|
98
|
+
buffer += decoder.decode(value, { stream: true });
|
|
99
|
+
|
|
100
|
+
// Split by newlines
|
|
101
|
+
const lines = buffer.split('\n');
|
|
102
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
103
|
+
|
|
104
|
+
// Process each complete line
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (line.trim()) {
|
|
107
|
+
try {
|
|
108
|
+
const event = JSON.parse(line);
|
|
109
|
+
|
|
110
|
+
// Append delta to output
|
|
111
|
+
if (event.delta) {
|
|
112
|
+
output.textContent += event.delta;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for completion
|
|
116
|
+
if (event.done) {
|
|
117
|
+
console.log('Stream marked as done');
|
|
118
|
+
output.textContent += '\n\n[Stream complete]';
|
|
119
|
+
reader = null;
|
|
120
|
+
startBtn.disabled = false;
|
|
121
|
+
stopBtn.disabled = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle errors
|
|
126
|
+
if (event.error) {
|
|
127
|
+
output.textContent += `\n\nError: ${event.error}`;
|
|
128
|
+
throw new Error(event.error);
|
|
129
|
+
}
|
|
130
|
+
} catch (parseError) {
|
|
131
|
+
console.error('Failed to parse JSON:', parseError, 'Line:', line);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Stream error:', error);
|
|
138
|
+
output.textContent += `\n\nError: ${error.message}`;
|
|
139
|
+
} finally {
|
|
140
|
+
reader = null;
|
|
141
|
+
startBtn.disabled = false;
|
|
142
|
+
stopBtn.disabled = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stopStream() {
|
|
147
|
+
if (reader) {
|
|
148
|
+
reader.cancel();
|
|
149
|
+
reader = null;
|
|
150
|
+
document.getElementById('output').textContent += '\n\n[Stream stopped by user]';
|
|
151
|
+
document.getElementById('startBtn').disabled = false;
|
|
152
|
+
document.getElementById('stopBtn').disabled = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|