@sylphx/lens-server 4.0.0 → 4.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 +76 -160
- package/dist/index.d.ts +138 -237
- package/dist/index.js +178 -313
- package/package.json +2 -2
- package/src/e2e/server.test.ts +12 -12
- package/src/handlers/http.test.ts +2 -2
- package/src/handlers/index.ts +3 -20
- package/src/handlers/ws.test.ts +3 -3
- package/src/index.ts +25 -41
- package/src/server/create.test.ts +34 -34
- package/src/server/create.ts +143 -14
- package/src/server/types.ts +34 -8
package/dist/index.js
CHANGED
|
@@ -39,12 +39,15 @@ import {
|
|
|
39
39
|
collectModelsFromRouter,
|
|
40
40
|
createEmit,
|
|
41
41
|
createResolverFromEntity,
|
|
42
|
+
firstValueFrom,
|
|
42
43
|
flattenRouter,
|
|
43
44
|
hashValue,
|
|
45
|
+
isError,
|
|
44
46
|
isLiveQueryDef,
|
|
45
47
|
isModelDef,
|
|
46
48
|
isMutationDef,
|
|
47
49
|
isQueryDef,
|
|
50
|
+
isSnapshot,
|
|
48
51
|
isSubscriptionDef,
|
|
49
52
|
mergeModelCollections,
|
|
50
53
|
toOps,
|
|
@@ -515,7 +518,7 @@ class LensServerImpl {
|
|
|
515
518
|
observer.complete?.();
|
|
516
519
|
return;
|
|
517
520
|
}
|
|
518
|
-
const publisher = subscriber({ input, ctx: context });
|
|
521
|
+
const publisher = subscriber({ args: input, ctx: context });
|
|
519
522
|
if (publisher) {
|
|
520
523
|
const emit = createEmit((command) => {
|
|
521
524
|
if (cancelled)
|
|
@@ -1009,353 +1012,91 @@ class LensServerImpl {
|
|
|
1009
1012
|
getPluginManager() {
|
|
1010
1013
|
return this.pluginManager;
|
|
1011
1014
|
}
|
|
1012
|
-
|
|
1013
|
-
function createApp(config) {
|
|
1014
|
-
const server = new LensServerImpl(config);
|
|
1015
|
-
return server;
|
|
1016
|
-
}
|
|
1017
|
-
// src/sse/handler.ts
|
|
1018
|
-
class SSEHandler {
|
|
1019
|
-
heartbeatInterval;
|
|
1020
|
-
onConnectCallback;
|
|
1021
|
-
onDisconnectCallback;
|
|
1022
|
-
clients = new Map;
|
|
1023
|
-
clientCounter = 0;
|
|
1024
|
-
constructor(config = {}) {
|
|
1025
|
-
this.heartbeatInterval = config.heartbeatInterval ?? 30000;
|
|
1026
|
-
this.onConnectCallback = config.onConnect;
|
|
1027
|
-
this.onDisconnectCallback = config.onDisconnect;
|
|
1028
|
-
}
|
|
1029
|
-
handleConnection(_req) {
|
|
1030
|
-
const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
|
|
1031
|
-
const encoder = new TextEncoder;
|
|
1032
|
-
const stream = new ReadableStream({
|
|
1033
|
-
start: (controller) => {
|
|
1034
|
-
const heartbeat = setInterval(() => {
|
|
1035
|
-
try {
|
|
1036
|
-
controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}
|
|
1037
|
-
|
|
1038
|
-
`));
|
|
1039
|
-
} catch {
|
|
1040
|
-
this.removeClient(clientId);
|
|
1041
|
-
}
|
|
1042
|
-
}, this.heartbeatInterval);
|
|
1043
|
-
this.clients.set(clientId, { controller, heartbeat, encoder });
|
|
1044
|
-
controller.enqueue(encoder.encode(`event: connected
|
|
1045
|
-
data: ${JSON.stringify({ clientId })}
|
|
1046
|
-
|
|
1047
|
-
`));
|
|
1048
|
-
const client = {
|
|
1049
|
-
id: clientId,
|
|
1050
|
-
send: (message) => this.send(clientId, message),
|
|
1051
|
-
sendEvent: (event, data) => this.sendEvent(clientId, event, data),
|
|
1052
|
-
close: () => this.closeClient(clientId)
|
|
1053
|
-
};
|
|
1054
|
-
this.onConnectCallback?.(client);
|
|
1055
|
-
},
|
|
1056
|
-
cancel: () => {
|
|
1057
|
-
this.removeClient(clientId);
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
return new Response(stream, {
|
|
1061
|
-
headers: {
|
|
1062
|
-
"Content-Type": "text/event-stream",
|
|
1063
|
-
"Cache-Control": "no-cache",
|
|
1064
|
-
Connection: "keep-alive",
|
|
1065
|
-
"Access-Control-Allow-Origin": "*"
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
send(clientId, message) {
|
|
1070
|
-
const client = this.clients.get(clientId);
|
|
1071
|
-
if (!client)
|
|
1072
|
-
return false;
|
|
1073
|
-
try {
|
|
1074
|
-
const data = `data: ${JSON.stringify(message)}
|
|
1075
|
-
|
|
1076
|
-
`;
|
|
1077
|
-
client.controller.enqueue(client.encoder.encode(data));
|
|
1078
|
-
return true;
|
|
1079
|
-
} catch {
|
|
1080
|
-
this.removeClient(clientId);
|
|
1081
|
-
return false;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
sendEvent(clientId, event, data) {
|
|
1085
|
-
const client = this.clients.get(clientId);
|
|
1086
|
-
if (!client)
|
|
1087
|
-
return false;
|
|
1088
|
-
if (/[\r\n:]/.test(event)) {
|
|
1089
|
-
return false;
|
|
1090
|
-
}
|
|
1091
|
-
try {
|
|
1092
|
-
const message = `event: ${event}
|
|
1093
|
-
data: ${JSON.stringify(data)}
|
|
1094
|
-
|
|
1095
|
-
`;
|
|
1096
|
-
client.controller.enqueue(client.encoder.encode(message));
|
|
1097
|
-
return true;
|
|
1098
|
-
} catch {
|
|
1099
|
-
this.removeClient(clientId);
|
|
1100
|
-
return false;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
broadcast(message) {
|
|
1104
|
-
for (const clientId of this.clients.keys()) {
|
|
1105
|
-
this.send(clientId, message);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
removeClient(clientId) {
|
|
1109
|
-
const client = this.clients.get(clientId);
|
|
1110
|
-
if (client) {
|
|
1111
|
-
clearInterval(client.heartbeat);
|
|
1112
|
-
this.clients.delete(clientId);
|
|
1113
|
-
this.onDisconnectCallback?.(clientId);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
closeClient(clientId) {
|
|
1117
|
-
const client = this.clients.get(clientId);
|
|
1118
|
-
if (client) {
|
|
1119
|
-
try {
|
|
1120
|
-
client.controller.close();
|
|
1121
|
-
} catch {}
|
|
1122
|
-
this.removeClient(clientId);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
getClientCount() {
|
|
1126
|
-
return this.clients.size;
|
|
1127
|
-
}
|
|
1128
|
-
getClientIds() {
|
|
1129
|
-
return Array.from(this.clients.keys());
|
|
1130
|
-
}
|
|
1131
|
-
hasClient(clientId) {
|
|
1132
|
-
return this.clients.has(clientId);
|
|
1133
|
-
}
|
|
1134
|
-
closeAll() {
|
|
1135
|
-
for (const clientId of this.clients.keys()) {
|
|
1136
|
-
this.closeClient(clientId);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function createSSEHandler(config = {}) {
|
|
1141
|
-
return new SSEHandler(config);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// src/handlers/http.ts
|
|
1145
|
-
import { firstValueFrom, isError, isSnapshot } from "@sylphx/lens-core";
|
|
1146
|
-
function sanitizeError(error, isDevelopment) {
|
|
1147
|
-
if (isDevelopment) {
|
|
1148
|
-
return error.message;
|
|
1149
|
-
}
|
|
1150
|
-
const message = error.message;
|
|
1151
|
-
const safePatterns = [
|
|
1152
|
-
/^Invalid input:/,
|
|
1153
|
-
/^Missing operation/,
|
|
1154
|
-
/^Not found/,
|
|
1155
|
-
/^Unauthorized/,
|
|
1156
|
-
/^Forbidden/,
|
|
1157
|
-
/^Bad request/,
|
|
1158
|
-
/^Validation failed/
|
|
1159
|
-
];
|
|
1160
|
-
if (safePatterns.some((pattern) => pattern.test(message))) {
|
|
1161
|
-
return message;
|
|
1162
|
-
}
|
|
1163
|
-
const sensitivePatterns = [
|
|
1164
|
-
/\/[^\s]+\.(ts|js|json)/,
|
|
1165
|
-
/at\s+[^\s]+\s+\(/,
|
|
1166
|
-
/ENOENT|EACCES|ECONNREFUSED/,
|
|
1167
|
-
/SELECT|INSERT|UPDATE|DELETE|FROM|WHERE/i,
|
|
1168
|
-
/password|secret|token|key|auth/i
|
|
1169
|
-
];
|
|
1170
|
-
if (sensitivePatterns.some((pattern) => pattern.test(message))) {
|
|
1171
|
-
return "An internal error occurred";
|
|
1172
|
-
}
|
|
1173
|
-
if (message.length < 100 && !message.includes(`
|
|
1174
|
-
`)) {
|
|
1175
|
-
return message;
|
|
1176
|
-
}
|
|
1177
|
-
return "An internal error occurred";
|
|
1178
|
-
}
|
|
1179
|
-
function createHTTPHandler(server, options = {}) {
|
|
1180
|
-
const { pathPrefix = "", cors, errors, health } = options;
|
|
1181
|
-
const isDevelopment = errors?.development ?? false;
|
|
1182
|
-
const healthEnabled = health?.enabled !== false;
|
|
1183
|
-
const healthPath = health?.path ?? "/__lens/health";
|
|
1184
|
-
const startTime = Date.now();
|
|
1185
|
-
const sanitize = (error) => {
|
|
1186
|
-
if (errors?.sanitize) {
|
|
1187
|
-
return errors.sanitize(error);
|
|
1188
|
-
}
|
|
1189
|
-
return sanitizeError(error, isDevelopment);
|
|
1190
|
-
};
|
|
1191
|
-
const allowedOrigin = cors?.origin ? Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin : isDevelopment ? "*" : "";
|
|
1192
|
-
const baseHeaders = {
|
|
1193
|
-
"Content-Type": "application/json",
|
|
1194
|
-
"X-Content-Type-Options": "nosniff",
|
|
1195
|
-
"X-Frame-Options": "DENY",
|
|
1196
|
-
"Access-Control-Allow-Methods": cors?.methods?.join(", ") ?? "GET, POST, OPTIONS",
|
|
1197
|
-
"Access-Control-Allow-Headers": cors?.headers?.join(", ") ?? "Content-Type, Authorization"
|
|
1198
|
-
};
|
|
1199
|
-
if (allowedOrigin) {
|
|
1200
|
-
baseHeaders["Access-Control-Allow-Origin"] = allowedOrigin;
|
|
1201
|
-
}
|
|
1202
|
-
const handler = async (request) => {
|
|
1015
|
+
fetch = async (request) => {
|
|
1203
1016
|
const url = new URL(request.url);
|
|
1204
1017
|
const pathname = url.pathname;
|
|
1018
|
+
const baseHeaders = {
|
|
1019
|
+
"Content-Type": "application/json",
|
|
1020
|
+
"X-Content-Type-Options": "nosniff",
|
|
1021
|
+
"X-Frame-Options": "DENY",
|
|
1022
|
+
"Access-Control-Allow-Origin": "*",
|
|
1023
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
1024
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
1025
|
+
};
|
|
1205
1026
|
if (request.method === "OPTIONS") {
|
|
1206
|
-
return new Response(null, {
|
|
1207
|
-
status: 204,
|
|
1208
|
-
headers: baseHeaders
|
|
1209
|
-
});
|
|
1027
|
+
return new Response(null, { status: 204, headers: baseHeaders });
|
|
1210
1028
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
let customChecks = {};
|
|
1216
|
-
let hasFailure = false;
|
|
1217
|
-
if (health?.checks) {
|
|
1218
|
-
try {
|
|
1219
|
-
customChecks = await health.checks();
|
|
1220
|
-
hasFailure = Object.values(customChecks).some((c) => c.status === "fail");
|
|
1221
|
-
} catch (error) {
|
|
1222
|
-
customChecks.healthCheck = {
|
|
1223
|
-
status: "fail",
|
|
1224
|
-
message: error instanceof Error ? error.message : "Health check failed"
|
|
1225
|
-
};
|
|
1226
|
-
hasFailure = true;
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
const response = {
|
|
1230
|
-
status: hasFailure ? "degraded" : "healthy",
|
|
1029
|
+
if (request.method === "GET" && pathname === "/__lens/health") {
|
|
1030
|
+
const metadata = this.getMetadata();
|
|
1031
|
+
return new Response(JSON.stringify({
|
|
1032
|
+
status: "healthy",
|
|
1231
1033
|
service: "lens-server",
|
|
1232
1034
|
version: metadata.version,
|
|
1233
|
-
uptime: uptimeSeconds,
|
|
1234
1035
|
timestamp: new Date().toISOString()
|
|
1235
|
-
}
|
|
1236
|
-
if (Object.keys(customChecks).length > 0) {
|
|
1237
|
-
response.checks = customChecks;
|
|
1238
|
-
}
|
|
1239
|
-
return new Response(JSON.stringify(response), {
|
|
1240
|
-
status: hasFailure ? 503 : 200,
|
|
1036
|
+
}), {
|
|
1241
1037
|
headers: {
|
|
1242
|
-
|
|
1243
|
-
"Cache-Control": "no-cache, no-store, must-revalidate"
|
|
1244
|
-
...baseHeaders
|
|
1038
|
+
...baseHeaders,
|
|
1039
|
+
"Cache-Control": "no-cache, no-store, must-revalidate"
|
|
1245
1040
|
}
|
|
1246
1041
|
});
|
|
1247
1042
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
headers: {
|
|
1252
|
-
"Content-Type": "application/json",
|
|
1253
|
-
...baseHeaders
|
|
1254
|
-
}
|
|
1043
|
+
if (request.method === "GET" && pathname === "/__lens/metadata") {
|
|
1044
|
+
return new Response(JSON.stringify(this.getMetadata()), {
|
|
1045
|
+
headers: baseHeaders
|
|
1255
1046
|
});
|
|
1256
1047
|
}
|
|
1257
|
-
|
|
1258
|
-
if (request.method === "POST" && (pathname === operationPath || pathname === `${pathPrefix}/`)) {
|
|
1048
|
+
if (request.method === "POST" && (pathname === "/" || pathname === "")) {
|
|
1259
1049
|
let body;
|
|
1260
1050
|
try {
|
|
1261
1051
|
body = await request.json();
|
|
1262
1052
|
} catch {
|
|
1263
1053
|
return new Response(JSON.stringify({ error: "Invalid JSON in request body" }), {
|
|
1264
1054
|
status: 400,
|
|
1265
|
-
headers:
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1055
|
+
headers: baseHeaders
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
if (!body.path) {
|
|
1059
|
+
return new Response(JSON.stringify({ error: "Missing operation path" }), {
|
|
1060
|
+
status: 400,
|
|
1061
|
+
headers: baseHeaders
|
|
1269
1062
|
});
|
|
1270
1063
|
}
|
|
1271
1064
|
try {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
headers: {
|
|
1276
|
-
"Content-Type": "application/json",
|
|
1277
|
-
...baseHeaders
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
const result2 = await firstValueFrom(server.execute({
|
|
1282
|
-
path: body.path,
|
|
1283
|
-
input: body.input
|
|
1284
|
-
}));
|
|
1285
|
-
if (isError(result2)) {
|
|
1286
|
-
return new Response(JSON.stringify({ error: result2.error }), {
|
|
1065
|
+
const result = await firstValueFrom(this.execute({ path: body.path, input: body.input }));
|
|
1066
|
+
if (isError(result)) {
|
|
1067
|
+
return new Response(JSON.stringify({ error: result.error }), {
|
|
1287
1068
|
status: 500,
|
|
1288
|
-
headers:
|
|
1289
|
-
"Content-Type": "application/json",
|
|
1290
|
-
...baseHeaders
|
|
1291
|
-
}
|
|
1069
|
+
headers: baseHeaders
|
|
1292
1070
|
});
|
|
1293
1071
|
}
|
|
1294
|
-
if (isSnapshot(
|
|
1295
|
-
return new Response(JSON.stringify({ data:
|
|
1296
|
-
headers:
|
|
1297
|
-
"Content-Type": "application/json",
|
|
1298
|
-
...baseHeaders
|
|
1299
|
-
}
|
|
1072
|
+
if (isSnapshot(result)) {
|
|
1073
|
+
return new Response(JSON.stringify({ data: result.data }), {
|
|
1074
|
+
headers: baseHeaders
|
|
1300
1075
|
});
|
|
1301
1076
|
}
|
|
1302
|
-
return new Response(JSON.stringify(
|
|
1303
|
-
headers: {
|
|
1304
|
-
"Content-Type": "application/json",
|
|
1305
|
-
...baseHeaders
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1077
|
+
return new Response(JSON.stringify(result), { headers: baseHeaders });
|
|
1308
1078
|
} catch (error) {
|
|
1309
|
-
const
|
|
1310
|
-
return new Response(JSON.stringify({ error:
|
|
1079
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1080
|
+
return new Response(JSON.stringify({ error: errMsg }), {
|
|
1311
1081
|
status: 500,
|
|
1312
|
-
headers:
|
|
1313
|
-
"Content-Type": "application/json",
|
|
1314
|
-
...baseHeaders
|
|
1315
|
-
}
|
|
1082
|
+
headers: baseHeaders
|
|
1316
1083
|
});
|
|
1317
1084
|
}
|
|
1318
1085
|
}
|
|
1319
1086
|
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
1320
1087
|
status: 404,
|
|
1321
|
-
headers:
|
|
1322
|
-
"Content-Type": "application/json",
|
|
1323
|
-
...baseHeaders
|
|
1324
|
-
}
|
|
1088
|
+
headers: baseHeaders
|
|
1325
1089
|
});
|
|
1326
1090
|
};
|
|
1327
|
-
const result = handler;
|
|
1328
|
-
result.handle = handler;
|
|
1329
|
-
return result;
|
|
1330
1091
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const sseHandler = new SSEHandler({
|
|
1340
|
-
...heartbeatInterval !== undefined && { heartbeatInterval },
|
|
1341
|
-
onConnect: (client) => {
|
|
1342
|
-
pluginManager.runOnConnect({ clientId: client.id });
|
|
1343
|
-
},
|
|
1344
|
-
onDisconnect: (clientId) => {
|
|
1345
|
-
pluginManager.runOnDisconnect({ clientId, subscriptionCount: 0 });
|
|
1346
|
-
}
|
|
1347
|
-
});
|
|
1348
|
-
const handler = async (request) => {
|
|
1349
|
-
const url = new URL(request.url);
|
|
1350
|
-
if (request.method === "GET" && url.pathname === fullSsePath) {
|
|
1351
|
-
return sseHandler.handleConnection(request);
|
|
1352
|
-
}
|
|
1353
|
-
return httpHandler(request);
|
|
1354
|
-
};
|
|
1355
|
-
const result = handler;
|
|
1356
|
-
result.handle = handler;
|
|
1357
|
-
result.sse = sseHandler;
|
|
1358
|
-
return result;
|
|
1092
|
+
function createApp(config) {
|
|
1093
|
+
const server = new LensServerImpl(config);
|
|
1094
|
+
const app = (request) => server.fetch(request);
|
|
1095
|
+
app.fetch = server.fetch;
|
|
1096
|
+
app.execute = server.execute.bind(server);
|
|
1097
|
+
app.getMetadata = server.getMetadata.bind(server);
|
|
1098
|
+
app.getPluginManager = server.getPluginManager.bind(server);
|
|
1099
|
+
return app;
|
|
1359
1100
|
}
|
|
1360
1101
|
// src/handlers/framework.ts
|
|
1361
1102
|
import { firstValueFrom as firstValueFrom2, isError as isError2, isSnapshot as isSnapshot2 } from "@sylphx/lens-core";
|
|
@@ -1514,6 +1255,132 @@ function createFrameworkHandler(server, options = {}) {
|
|
|
1514
1255
|
return new Response("Method not allowed", { status: 405 });
|
|
1515
1256
|
};
|
|
1516
1257
|
}
|
|
1258
|
+
// src/sse/handler.ts
|
|
1259
|
+
class SSEHandler {
|
|
1260
|
+
heartbeatInterval;
|
|
1261
|
+
onConnectCallback;
|
|
1262
|
+
onDisconnectCallback;
|
|
1263
|
+
clients = new Map;
|
|
1264
|
+
clientCounter = 0;
|
|
1265
|
+
constructor(config = {}) {
|
|
1266
|
+
this.heartbeatInterval = config.heartbeatInterval ?? 30000;
|
|
1267
|
+
this.onConnectCallback = config.onConnect;
|
|
1268
|
+
this.onDisconnectCallback = config.onDisconnect;
|
|
1269
|
+
}
|
|
1270
|
+
handleConnection(_req) {
|
|
1271
|
+
const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
|
|
1272
|
+
const encoder = new TextEncoder;
|
|
1273
|
+
const stream = new ReadableStream({
|
|
1274
|
+
start: (controller) => {
|
|
1275
|
+
const heartbeat = setInterval(() => {
|
|
1276
|
+
try {
|
|
1277
|
+
controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}
|
|
1278
|
+
|
|
1279
|
+
`));
|
|
1280
|
+
} catch {
|
|
1281
|
+
this.removeClient(clientId);
|
|
1282
|
+
}
|
|
1283
|
+
}, this.heartbeatInterval);
|
|
1284
|
+
this.clients.set(clientId, { controller, heartbeat, encoder });
|
|
1285
|
+
controller.enqueue(encoder.encode(`event: connected
|
|
1286
|
+
data: ${JSON.stringify({ clientId })}
|
|
1287
|
+
|
|
1288
|
+
`));
|
|
1289
|
+
const client = {
|
|
1290
|
+
id: clientId,
|
|
1291
|
+
send: (message) => this.send(clientId, message),
|
|
1292
|
+
sendEvent: (event, data) => this.sendEvent(clientId, event, data),
|
|
1293
|
+
close: () => this.closeClient(clientId)
|
|
1294
|
+
};
|
|
1295
|
+
this.onConnectCallback?.(client);
|
|
1296
|
+
},
|
|
1297
|
+
cancel: () => {
|
|
1298
|
+
this.removeClient(clientId);
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
return new Response(stream, {
|
|
1302
|
+
headers: {
|
|
1303
|
+
"Content-Type": "text/event-stream",
|
|
1304
|
+
"Cache-Control": "no-cache",
|
|
1305
|
+
Connection: "keep-alive",
|
|
1306
|
+
"Access-Control-Allow-Origin": "*"
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
send(clientId, message) {
|
|
1311
|
+
const client = this.clients.get(clientId);
|
|
1312
|
+
if (!client)
|
|
1313
|
+
return false;
|
|
1314
|
+
try {
|
|
1315
|
+
const data = `data: ${JSON.stringify(message)}
|
|
1316
|
+
|
|
1317
|
+
`;
|
|
1318
|
+
client.controller.enqueue(client.encoder.encode(data));
|
|
1319
|
+
return true;
|
|
1320
|
+
} catch {
|
|
1321
|
+
this.removeClient(clientId);
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
sendEvent(clientId, event, data) {
|
|
1326
|
+
const client = this.clients.get(clientId);
|
|
1327
|
+
if (!client)
|
|
1328
|
+
return false;
|
|
1329
|
+
if (/[\r\n:]/.test(event)) {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
const message = `event: ${event}
|
|
1334
|
+
data: ${JSON.stringify(data)}
|
|
1335
|
+
|
|
1336
|
+
`;
|
|
1337
|
+
client.controller.enqueue(client.encoder.encode(message));
|
|
1338
|
+
return true;
|
|
1339
|
+
} catch {
|
|
1340
|
+
this.removeClient(clientId);
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
broadcast(message) {
|
|
1345
|
+
for (const clientId of this.clients.keys()) {
|
|
1346
|
+
this.send(clientId, message);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
removeClient(clientId) {
|
|
1350
|
+
const client = this.clients.get(clientId);
|
|
1351
|
+
if (client) {
|
|
1352
|
+
clearInterval(client.heartbeat);
|
|
1353
|
+
this.clients.delete(clientId);
|
|
1354
|
+
this.onDisconnectCallback?.(clientId);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
closeClient(clientId) {
|
|
1358
|
+
const client = this.clients.get(clientId);
|
|
1359
|
+
if (client) {
|
|
1360
|
+
try {
|
|
1361
|
+
client.controller.close();
|
|
1362
|
+
} catch {}
|
|
1363
|
+
this.removeClient(clientId);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
getClientCount() {
|
|
1367
|
+
return this.clients.size;
|
|
1368
|
+
}
|
|
1369
|
+
getClientIds() {
|
|
1370
|
+
return Array.from(this.clients.keys());
|
|
1371
|
+
}
|
|
1372
|
+
hasClient(clientId) {
|
|
1373
|
+
return this.clients.has(clientId);
|
|
1374
|
+
}
|
|
1375
|
+
closeAll() {
|
|
1376
|
+
for (const clientId of this.clients.keys()) {
|
|
1377
|
+
this.closeClient(clientId);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function createSSEHandler(config = {}) {
|
|
1382
|
+
return new SSEHandler(config);
|
|
1383
|
+
}
|
|
1517
1384
|
// src/handlers/ws.ts
|
|
1518
1385
|
import {
|
|
1519
1386
|
firstValueFrom as firstValueFrom3,
|
|
@@ -2961,8 +2828,6 @@ export {
|
|
|
2961
2828
|
createServerClientProxy,
|
|
2962
2829
|
createSSEHandler,
|
|
2963
2830
|
createPluginManager,
|
|
2964
|
-
createHandler,
|
|
2965
|
-
createHTTPHandler,
|
|
2966
2831
|
createFrameworkHandler,
|
|
2967
2832
|
createContext,
|
|
2968
2833
|
createApp,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Server runtime for Lens API framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "SylphxAI",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@sylphx/lens-core": "^4.0
|
|
33
|
+
"@sylphx/lens-core": "^4.1.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.9.3",
|
package/src/e2e/server.test.ts
CHANGED
|
@@ -68,7 +68,7 @@ describe("E2E - Basic Operations", () => {
|
|
|
68
68
|
|
|
69
69
|
it("query with input", async () => {
|
|
70
70
|
const getUser = query()
|
|
71
|
-
.
|
|
71
|
+
.args(z.object({ id: z.string() }))
|
|
72
72
|
.returns(User)
|
|
73
73
|
.resolve(({ args }) => {
|
|
74
74
|
const user = mockUsers.find((u) => u.id === args.id);
|
|
@@ -96,7 +96,7 @@ describe("E2E - Basic Operations", () => {
|
|
|
96
96
|
|
|
97
97
|
it("mutation", async () => {
|
|
98
98
|
const createUser = mutation()
|
|
99
|
-
.
|
|
99
|
+
.args(z.object({ name: z.string(), email: z.string() }))
|
|
100
100
|
.returns(User)
|
|
101
101
|
.resolve(({ args }) => ({
|
|
102
102
|
id: "user-new",
|
|
@@ -130,7 +130,7 @@ describe("E2E - Basic Operations", () => {
|
|
|
130
130
|
|
|
131
131
|
it("handles query errors", async () => {
|
|
132
132
|
const failingQuery = query()
|
|
133
|
-
.
|
|
133
|
+
.args(z.object({ id: z.string() }))
|
|
134
134
|
.resolve(() => {
|
|
135
135
|
throw new Error("Query failed");
|
|
136
136
|
});
|
|
@@ -178,7 +178,7 @@ describe("E2E - Context", () => {
|
|
|
178
178
|
let capturedContext: unknown = null;
|
|
179
179
|
|
|
180
180
|
const getUser = query()
|
|
181
|
-
.
|
|
181
|
+
.args(z.object({ id: z.string() }))
|
|
182
182
|
.resolve(({ ctx }) => {
|
|
183
183
|
capturedContext = ctx;
|
|
184
184
|
return mockUsers[0];
|
|
@@ -206,7 +206,7 @@ describe("E2E - Context", () => {
|
|
|
206
206
|
let capturedContext: unknown = null;
|
|
207
207
|
|
|
208
208
|
const getUser = query()
|
|
209
|
-
.
|
|
209
|
+
.args(z.object({ id: z.string() }))
|
|
210
210
|
.resolve(({ ctx }) => {
|
|
211
211
|
capturedContext = ctx;
|
|
212
212
|
return mockUsers[0];
|
|
@@ -240,7 +240,7 @@ describe("E2E - Context", () => {
|
|
|
240
240
|
describe("E2E - Selection", () => {
|
|
241
241
|
it("applies $select to filter fields", async () => {
|
|
242
242
|
const getUser = query()
|
|
243
|
-
.
|
|
243
|
+
.args(z.object({ id: z.string() }))
|
|
244
244
|
.returns(User)
|
|
245
245
|
.resolve(({ args }) => {
|
|
246
246
|
const user = mockUsers.find((u) => u.id === args.id);
|
|
@@ -278,7 +278,7 @@ describe("E2E - Selection", () => {
|
|
|
278
278
|
|
|
279
279
|
it("includes id by default in selection", async () => {
|
|
280
280
|
const getUser = query()
|
|
281
|
-
.
|
|
281
|
+
.args(z.object({ id: z.string() }))
|
|
282
282
|
.returns(User)
|
|
283
283
|
.resolve(({ args }) => mockUsers.find((u) => u.id === args.id)!);
|
|
284
284
|
|
|
@@ -340,7 +340,7 @@ describe("E2E - Entity Resolvers", () => {
|
|
|
340
340
|
}));
|
|
341
341
|
|
|
342
342
|
const getUser = query()
|
|
343
|
-
.
|
|
343
|
+
.args(z.object({ id: z.string() }))
|
|
344
344
|
.returns(UserWithPosts)
|
|
345
345
|
.resolve(({ args }) => {
|
|
346
346
|
const user = users.find((u) => u.id === args.id);
|
|
@@ -460,12 +460,12 @@ describe("E2E - Entity Resolvers", () => {
|
|
|
460
460
|
describe("E2E - Metadata", () => {
|
|
461
461
|
it("returns correct metadata structure", () => {
|
|
462
462
|
const getUser = query()
|
|
463
|
-
.
|
|
463
|
+
.args(z.object({ id: z.string() }))
|
|
464
464
|
.returns(User)
|
|
465
465
|
.resolve(({ args }) => mockUsers.find((u) => u.id === args.id)!);
|
|
466
466
|
|
|
467
467
|
const createUser = mutation()
|
|
468
|
-
.
|
|
468
|
+
.args(z.object({ name: z.string() }))
|
|
469
469
|
.returns(User)
|
|
470
470
|
.resolve(({ args }) => ({ id: "new", name: args.name, email: "", status: "" }));
|
|
471
471
|
|
|
@@ -489,12 +489,12 @@ describe("E2E - Metadata", () => {
|
|
|
489
489
|
|
|
490
490
|
it("auto-derives optimistic hints from naming with plugin", () => {
|
|
491
491
|
const updateUser = mutation()
|
|
492
|
-
.
|
|
492
|
+
.args(z.object({ id: z.string(), name: z.string() }))
|
|
493
493
|
.returns(User)
|
|
494
494
|
.resolve(({ args }) => ({ ...mockUsers[0], name: args.name }));
|
|
495
495
|
|
|
496
496
|
const deleteUser = mutation()
|
|
497
|
-
.
|
|
497
|
+
.args(z.object({ id: z.string() }))
|
|
498
498
|
.resolve(() => ({ success: true }));
|
|
499
499
|
|
|
500
500
|
const server = createApp({
|
|
@@ -13,14 +13,14 @@ import { createHTTPHandler } from "./http.js";
|
|
|
13
13
|
// =============================================================================
|
|
14
14
|
|
|
15
15
|
const getUser = query()
|
|
16
|
-
.
|
|
16
|
+
.args(z.object({ id: z.string() }))
|
|
17
17
|
.resolve(({ args }) => ({
|
|
18
18
|
id: args.id,
|
|
19
19
|
name: "Test User",
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
const createUser = mutation()
|
|
23
|
-
.
|
|
23
|
+
.args(z.object({ name: z.string() }))
|
|
24
24
|
.resolve(({ args }) => ({
|
|
25
25
|
id: "new-id",
|
|
26
26
|
name: args.name,
|