@sylphx/lens-server 1.0.2

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 ADDED
@@ -0,0 +1,1437 @@
1
+ // ../core/dist/index.js
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ var ENTITY_SYMBOL = Symbol("lens:entity");
4
+ var valueStrategy = {
5
+ name: "value",
6
+ encode(_prev, next) {
7
+ return { strategy: "value", data: next };
8
+ },
9
+ decode(_current, update) {
10
+ return update.data;
11
+ },
12
+ estimateSize(update) {
13
+ return JSON.stringify(update.data).length;
14
+ }
15
+ };
16
+ var deltaStrategy = {
17
+ name: "delta",
18
+ encode(prev, next) {
19
+ const operations = computeStringDiff(prev, next);
20
+ const diffSize = JSON.stringify(operations).length;
21
+ const valueSize = next.length + 20;
22
+ if (diffSize >= valueSize) {
23
+ return { strategy: "value", data: next };
24
+ }
25
+ return { strategy: "delta", data: operations };
26
+ },
27
+ decode(current, update) {
28
+ if (update.strategy === "value") {
29
+ return update.data;
30
+ }
31
+ const operations = update.data;
32
+ return applyStringDiff(current, operations);
33
+ },
34
+ estimateSize(update) {
35
+ return JSON.stringify(update.data).length;
36
+ }
37
+ };
38
+ function computeStringDiff(prev, next) {
39
+ const operations = [];
40
+ let prefixLen = 0;
41
+ const minLen = Math.min(prev.length, next.length);
42
+ while (prefixLen < minLen && prev[prefixLen] === next[prefixLen]) {
43
+ prefixLen++;
44
+ }
45
+ let suffixLen = 0;
46
+ const remainingPrev = prev.length - prefixLen;
47
+ const remainingNext = next.length - prefixLen;
48
+ const maxSuffix = Math.min(remainingPrev, remainingNext);
49
+ while (suffixLen < maxSuffix && prev[prev.length - 1 - suffixLen] === next[next.length - 1 - suffixLen]) {
50
+ suffixLen++;
51
+ }
52
+ const deleteCount = prev.length - prefixLen - suffixLen;
53
+ const insertText = next.slice(prefixLen, next.length - suffixLen || undefined);
54
+ if (deleteCount > 0 || insertText.length > 0) {
55
+ operations.push({
56
+ position: prefixLen,
57
+ ...deleteCount > 0 ? { delete: deleteCount } : {},
58
+ ...insertText.length > 0 ? { insert: insertText } : {}
59
+ });
60
+ }
61
+ return operations;
62
+ }
63
+ function applyStringDiff(current, operations) {
64
+ let result = current;
65
+ const sortedOps = [...operations].sort((a, b) => b.position - a.position);
66
+ for (const op of sortedOps) {
67
+ const before = result.slice(0, op.position);
68
+ const after = result.slice(op.position + (op.delete ?? 0));
69
+ result = before + (op.insert ?? "") + after;
70
+ }
71
+ return result;
72
+ }
73
+ var patchStrategy = {
74
+ name: "patch",
75
+ encode(prev, next) {
76
+ const operations = computeJsonPatch(prev, next);
77
+ const patchSize = JSON.stringify(operations).length;
78
+ const valueSize = JSON.stringify(next).length + 20;
79
+ if (patchSize >= valueSize) {
80
+ return { strategy: "value", data: next };
81
+ }
82
+ return { strategy: "patch", data: operations };
83
+ },
84
+ decode(current, update) {
85
+ if (update.strategy === "value") {
86
+ return update.data;
87
+ }
88
+ const operations = update.data;
89
+ return applyJsonPatch(current, operations);
90
+ },
91
+ estimateSize(update) {
92
+ return JSON.stringify(update.data).length;
93
+ }
94
+ };
95
+ function computeJsonPatch(prev, next, basePath = "") {
96
+ const operations = [];
97
+ const prevObj = prev;
98
+ const nextObj = next;
99
+ for (const key of Object.keys(prevObj)) {
100
+ if (!(key in nextObj)) {
101
+ operations.push({ op: "remove", path: `${basePath}/${escapeJsonPointer(key)}` });
102
+ }
103
+ }
104
+ for (const [key, nextValue] of Object.entries(nextObj)) {
105
+ const path = `${basePath}/${escapeJsonPointer(key)}`;
106
+ const prevValue = prevObj[key];
107
+ if (!(key in prevObj)) {
108
+ operations.push({ op: "add", path, value: nextValue });
109
+ } else if (!deepEqual(prevValue, nextValue)) {
110
+ if (isPlainObject(prevValue) && isPlainObject(nextValue) && !Array.isArray(prevValue) && !Array.isArray(nextValue)) {
111
+ operations.push(...computeJsonPatch(prevValue, nextValue, path));
112
+ } else {
113
+ operations.push({ op: "replace", path, value: nextValue });
114
+ }
115
+ }
116
+ }
117
+ return operations;
118
+ }
119
+ function applyJsonPatch(current, operations) {
120
+ const result = structuredClone(current);
121
+ for (const op of operations) {
122
+ const pathParts = parseJsonPointer(op.path);
123
+ switch (op.op) {
124
+ case "add":
125
+ case "replace":
126
+ setValueAtPath(result, pathParts, op.value);
127
+ break;
128
+ case "remove":
129
+ removeValueAtPath(result, pathParts);
130
+ break;
131
+ case "move":
132
+ if (op.from) {
133
+ const fromParts = parseJsonPointer(op.from);
134
+ const value = getValueAtPath(result, fromParts);
135
+ removeValueAtPath(result, fromParts);
136
+ setValueAtPath(result, pathParts, value);
137
+ }
138
+ break;
139
+ case "copy":
140
+ if (op.from) {
141
+ const fromParts = parseJsonPointer(op.from);
142
+ const value = structuredClone(getValueAtPath(result, fromParts));
143
+ setValueAtPath(result, pathParts, value);
144
+ }
145
+ break;
146
+ case "test":
147
+ break;
148
+ }
149
+ }
150
+ return result;
151
+ }
152
+ var THRESHOLDS = {
153
+ STRING_DELTA_MIN: 100,
154
+ OBJECT_PATCH_MIN: 50
155
+ };
156
+ function selectStrategy(prev, next) {
157
+ if (typeof prev === "string" && typeof next === "string") {
158
+ if (next.length >= THRESHOLDS.STRING_DELTA_MIN) {
159
+ return deltaStrategy;
160
+ }
161
+ return valueStrategy;
162
+ }
163
+ if (typeof next !== "object" || next === null) {
164
+ return valueStrategy;
165
+ }
166
+ if (isPlainObject(prev) && isPlainObject(next)) {
167
+ const prevSize = JSON.stringify(prev).length;
168
+ if (prevSize >= THRESHOLDS.OBJECT_PATCH_MIN) {
169
+ return patchStrategy;
170
+ }
171
+ }
172
+ return valueStrategy;
173
+ }
174
+ function createUpdate(prev, next) {
175
+ const strategy = selectStrategy(prev, next);
176
+ return strategy.encode(prev, next);
177
+ }
178
+ function isPlainObject(value) {
179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
180
+ }
181
+ function deepEqual(a, b) {
182
+ if (a === b)
183
+ return true;
184
+ if (typeof a !== typeof b)
185
+ return false;
186
+ if (typeof a !== "object" || a === null || b === null)
187
+ return false;
188
+ const aKeys = Object.keys(a);
189
+ const bKeys = Object.keys(b);
190
+ if (aKeys.length !== bKeys.length)
191
+ return false;
192
+ for (const key of aKeys) {
193
+ if (!deepEqual(a[key], b[key])) {
194
+ return false;
195
+ }
196
+ }
197
+ return true;
198
+ }
199
+ function escapeJsonPointer(str) {
200
+ return str.replace(/~/g, "~0").replace(/\//g, "~1");
201
+ }
202
+ function parseJsonPointer(path) {
203
+ if (!path || path === "/")
204
+ return [];
205
+ return path.slice(1).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
206
+ }
207
+ function getValueAtPath(obj, path) {
208
+ let current = obj;
209
+ for (const key of path) {
210
+ if (current === null || typeof current !== "object")
211
+ return;
212
+ current = current[key];
213
+ }
214
+ return current;
215
+ }
216
+ function setValueAtPath(obj, path, value) {
217
+ if (path.length === 0)
218
+ return;
219
+ let current = obj;
220
+ for (let i = 0;i < path.length - 1; i++) {
221
+ const key = path[i];
222
+ if (!(key in current) || typeof current[key] !== "object") {
223
+ current[key] = {};
224
+ }
225
+ current = current[key];
226
+ }
227
+ current[path[path.length - 1]] = value;
228
+ }
229
+ function removeValueAtPath(obj, path) {
230
+ if (path.length === 0)
231
+ return;
232
+ let current = obj;
233
+ for (let i = 0;i < path.length - 1; i++) {
234
+ const key = path[i];
235
+ if (!(key in current) || typeof current[key] !== "object")
236
+ return;
237
+ current = current[key];
238
+ }
239
+ delete current[path[path.length - 1]];
240
+ }
241
+ function isQueryDef(value) {
242
+ return typeof value === "object" && value !== null && value._type === "query";
243
+ }
244
+ function isMutationDef(value) {
245
+ return typeof value === "object" && value !== null && value._type === "mutation";
246
+ }
247
+ function isRouterDef(value) {
248
+ return typeof value === "object" && value !== null && value._type === "router";
249
+ }
250
+ function flattenRouter(routerDef, prefix = "") {
251
+ const result = new Map;
252
+ for (const [key, value] of Object.entries(routerDef._routes)) {
253
+ const path = prefix ? `${prefix}.${key}` : key;
254
+ if (isRouterDef(value)) {
255
+ const nested = flattenRouter(value, path);
256
+ for (const [nestedPath, procedure] of nested) {
257
+ result.set(nestedPath, procedure);
258
+ }
259
+ } else {
260
+ result.set(path, value);
261
+ }
262
+ }
263
+ return result;
264
+ }
265
+ function isBatchResolver(resolver) {
266
+ return typeof resolver === "object" && resolver !== null && "batch" in resolver;
267
+ }
268
+ var globalContextStore = new AsyncLocalStorage;
269
+ function createContext() {
270
+ return globalContextStore;
271
+ }
272
+ function runWithContext(_context, value, fn) {
273
+ return globalContextStore.run(value, fn);
274
+ }
275
+ function makeEntityKey(entity2, id) {
276
+ return `${entity2}:${id}`;
277
+ }
278
+
279
+ // src/state/graph-state-manager.ts
280
+ class GraphStateManager {
281
+ clients = new Map;
282
+ canonical = new Map;
283
+ clientStates = new Map;
284
+ entitySubscribers = new Map;
285
+ config;
286
+ constructor(config = {}) {
287
+ this.config = config;
288
+ }
289
+ addClient(client) {
290
+ this.clients.set(client.id, client);
291
+ this.clientStates.set(client.id, new Map);
292
+ }
293
+ removeClient(clientId) {
294
+ for (const [key, subscribers] of this.entitySubscribers) {
295
+ subscribers.delete(clientId);
296
+ if (subscribers.size === 0) {
297
+ this.cleanupEntity(key);
298
+ }
299
+ }
300
+ this.clients.delete(clientId);
301
+ this.clientStates.delete(clientId);
302
+ }
303
+ subscribe(clientId, entity, id, fields = "*") {
304
+ const key = this.makeKey(entity, id);
305
+ let subscribers = this.entitySubscribers.get(key);
306
+ if (!subscribers) {
307
+ subscribers = new Set;
308
+ this.entitySubscribers.set(key, subscribers);
309
+ }
310
+ subscribers.add(clientId);
311
+ const clientStateMap = this.clientStates.get(clientId);
312
+ if (clientStateMap) {
313
+ const fieldSet = fields === "*" ? "*" : new Set(fields);
314
+ clientStateMap.set(key, {
315
+ lastState: {},
316
+ fields: fieldSet
317
+ });
318
+ }
319
+ const canonicalState = this.canonical.get(key);
320
+ if (canonicalState) {
321
+ this.sendInitialData(clientId, entity, id, canonicalState, fields);
322
+ }
323
+ }
324
+ unsubscribe(clientId, entity, id) {
325
+ const key = this.makeKey(entity, id);
326
+ const subscribers = this.entitySubscribers.get(key);
327
+ if (subscribers) {
328
+ subscribers.delete(clientId);
329
+ if (subscribers.size === 0) {
330
+ this.cleanupEntity(key);
331
+ }
332
+ }
333
+ const clientStateMap = this.clientStates.get(clientId);
334
+ if (clientStateMap) {
335
+ clientStateMap.delete(key);
336
+ }
337
+ }
338
+ updateSubscription(clientId, entity, id, fields) {
339
+ const key = this.makeKey(entity, id);
340
+ const clientStateMap = this.clientStates.get(clientId);
341
+ if (clientStateMap) {
342
+ const state = clientStateMap.get(key);
343
+ if (state) {
344
+ state.fields = fields === "*" ? "*" : new Set(fields);
345
+ }
346
+ }
347
+ }
348
+ emit(entity, id, data, options = {}) {
349
+ const key = this.makeKey(entity, id);
350
+ let currentCanonical = this.canonical.get(key);
351
+ if (options.replace || !currentCanonical) {
352
+ currentCanonical = { ...data };
353
+ } else {
354
+ currentCanonical = { ...currentCanonical, ...data };
355
+ }
356
+ this.canonical.set(key, currentCanonical);
357
+ const subscribers = this.entitySubscribers.get(key);
358
+ if (!subscribers)
359
+ return;
360
+ for (const clientId of subscribers) {
361
+ this.pushToClient(clientId, entity, id, key, currentCanonical);
362
+ }
363
+ }
364
+ getState(entity, id) {
365
+ return this.canonical.get(this.makeKey(entity, id));
366
+ }
367
+ hasSubscribers(entity, id) {
368
+ const subscribers = this.entitySubscribers.get(this.makeKey(entity, id));
369
+ return subscribers !== undefined && subscribers.size > 0;
370
+ }
371
+ pushToClient(clientId, entity, id, key, newState) {
372
+ const client = this.clients.get(clientId);
373
+ if (!client)
374
+ return;
375
+ const clientStateMap = this.clientStates.get(clientId);
376
+ if (!clientStateMap)
377
+ return;
378
+ const clientEntityState = clientStateMap.get(key);
379
+ if (!clientEntityState)
380
+ return;
381
+ const { lastState, fields } = clientEntityState;
382
+ const fieldsToCheck = fields === "*" ? Object.keys(newState) : Array.from(fields);
383
+ const updates = {};
384
+ let hasChanges = false;
385
+ for (const field of fieldsToCheck) {
386
+ const oldValue = lastState[field];
387
+ const newValue = newState[field];
388
+ if (oldValue === newValue)
389
+ continue;
390
+ if (typeof oldValue === "object" && typeof newValue === "object" && JSON.stringify(oldValue) === JSON.stringify(newValue)) {
391
+ continue;
392
+ }
393
+ const update = createUpdate(oldValue, newValue);
394
+ updates[field] = update;
395
+ hasChanges = true;
396
+ }
397
+ if (!hasChanges)
398
+ return;
399
+ client.send({
400
+ type: "update",
401
+ entity,
402
+ id,
403
+ updates
404
+ });
405
+ for (const field of fieldsToCheck) {
406
+ if (newState[field] !== undefined) {
407
+ clientEntityState.lastState[field] = newState[field];
408
+ }
409
+ }
410
+ }
411
+ sendInitialData(clientId, entity, id, state, fields) {
412
+ const client = this.clients.get(clientId);
413
+ if (!client)
414
+ return;
415
+ const key = this.makeKey(entity, id);
416
+ const clientStateMap = this.clientStates.get(clientId);
417
+ if (!clientStateMap)
418
+ return;
419
+ const fieldsToSend = fields === "*" ? Object.keys(state) : fields;
420
+ const dataToSend = {};
421
+ const updates = {};
422
+ for (const field of fieldsToSend) {
423
+ if (state[field] !== undefined) {
424
+ dataToSend[field] = state[field];
425
+ updates[field] = { strategy: "value", data: state[field] };
426
+ }
427
+ }
428
+ client.send({
429
+ type: "update",
430
+ entity,
431
+ id,
432
+ updates
433
+ });
434
+ const clientEntityState = clientStateMap.get(key);
435
+ if (clientEntityState) {
436
+ clientEntityState.lastState = { ...dataToSend };
437
+ }
438
+ }
439
+ cleanupEntity(key) {
440
+ const [entity, id] = key.split(":");
441
+ if (this.config.onEntityUnsubscribed) {
442
+ this.config.onEntityUnsubscribed(entity, id);
443
+ }
444
+ this.entitySubscribers.delete(key);
445
+ }
446
+ makeKey(entity, id) {
447
+ return makeEntityKey(entity, id);
448
+ }
449
+ getStats() {
450
+ let totalSubscriptions = 0;
451
+ for (const subscribers of this.entitySubscribers.values()) {
452
+ totalSubscriptions += subscribers.size;
453
+ }
454
+ return {
455
+ clients: this.clients.size,
456
+ entities: this.canonical.size,
457
+ totalSubscriptions
458
+ };
459
+ }
460
+ clear() {
461
+ this.clients.clear();
462
+ this.canonical.clear();
463
+ this.clientStates.clear();
464
+ this.entitySubscribers.clear();
465
+ }
466
+ }
467
+ function createGraphStateManager(config) {
468
+ return new GraphStateManager(config);
469
+ }
470
+
471
+ // src/server/create.ts
472
+ class DataLoader {
473
+ batchFn;
474
+ batch = new Map;
475
+ scheduled = false;
476
+ constructor(batchFn) {
477
+ this.batchFn = batchFn;
478
+ }
479
+ async load(key) {
480
+ return new Promise((resolve, reject) => {
481
+ const existing = this.batch.get(key);
482
+ if (existing) {
483
+ existing.push({ resolve, reject });
484
+ } else {
485
+ this.batch.set(key, [{ resolve, reject }]);
486
+ }
487
+ this.scheduleDispatch();
488
+ });
489
+ }
490
+ scheduleDispatch() {
491
+ if (this.scheduled)
492
+ return;
493
+ this.scheduled = true;
494
+ queueMicrotask(() => this.dispatch());
495
+ }
496
+ async dispatch() {
497
+ this.scheduled = false;
498
+ const batch = this.batch;
499
+ this.batch = new Map;
500
+ const keys = Array.from(batch.keys());
501
+ if (keys.length === 0)
502
+ return;
503
+ try {
504
+ const results = await this.batchFn(keys);
505
+ keys.forEach((key, index) => {
506
+ const callbacks = batch.get(key);
507
+ const result = results[index] ?? null;
508
+ callbacks.forEach(({ resolve }) => resolve(result));
509
+ });
510
+ } catch (error) {
511
+ for (const callbacks of batch.values()) {
512
+ callbacks.forEach(({ reject }) => reject(error));
513
+ }
514
+ }
515
+ }
516
+ clear() {
517
+ this.batch.clear();
518
+ }
519
+ }
520
+
521
+ class LensServerImpl {
522
+ queries;
523
+ mutations;
524
+ entities;
525
+ resolvers;
526
+ contextFactory;
527
+ version;
528
+ ctx = createContext();
529
+ stateManager;
530
+ loaders = new Map;
531
+ connections = new Map;
532
+ connectionCounter = 0;
533
+ server = null;
534
+ constructor(config) {
535
+ const queries = { ...config.queries ?? {} };
536
+ const mutations = { ...config.mutations ?? {} };
537
+ if (config.router) {
538
+ const flattened = flattenRouter(config.router);
539
+ for (const [path, procedure] of flattened) {
540
+ if (isQueryDef(procedure)) {
541
+ queries[path] = procedure;
542
+ } else if (isMutationDef(procedure)) {
543
+ mutations[path] = procedure;
544
+ }
545
+ }
546
+ }
547
+ this.queries = queries;
548
+ this.mutations = mutations;
549
+ this.entities = config.entities ?? {};
550
+ this.resolvers = config.resolvers;
551
+ this.contextFactory = config.context ?? (() => ({}));
552
+ this.version = config.version ?? "1.0.0";
553
+ for (const [name, def] of Object.entries(this.entities)) {
554
+ if (def && typeof def === "object" && !def._name) {
555
+ def._name = name;
556
+ }
557
+ }
558
+ for (const [name, def] of Object.entries(this.mutations)) {
559
+ if (def && typeof def === "object") {
560
+ def._name = name;
561
+ const lastSegment = name.includes(".") ? name.split(".").pop() : name;
562
+ if (!def._optimistic) {
563
+ if (lastSegment.startsWith("update")) {
564
+ def._optimistic = "merge";
565
+ } else if (lastSegment.startsWith("create") || lastSegment.startsWith("add")) {
566
+ def._optimistic = "create";
567
+ } else if (lastSegment.startsWith("delete") || lastSegment.startsWith("remove")) {
568
+ def._optimistic = "delete";
569
+ }
570
+ }
571
+ }
572
+ }
573
+ for (const [name, def] of Object.entries(this.queries)) {
574
+ if (def && typeof def === "object") {
575
+ def._name = name;
576
+ }
577
+ }
578
+ this.stateManager = new GraphStateManager({
579
+ onEntityUnsubscribed: (_entity, _id) => {}
580
+ });
581
+ for (const [name, def] of Object.entries(this.queries)) {
582
+ if (!isQueryDef(def)) {
583
+ throw new Error(`Invalid query definition: ${name}`);
584
+ }
585
+ }
586
+ for (const [name, def] of Object.entries(this.mutations)) {
587
+ if (!isMutationDef(def)) {
588
+ throw new Error(`Invalid mutation definition: ${name}`);
589
+ }
590
+ }
591
+ }
592
+ getStateManager() {
593
+ return this.stateManager;
594
+ }
595
+ getMetadata() {
596
+ return {
597
+ version: this.version,
598
+ operations: this.buildOperationsMap()
599
+ };
600
+ }
601
+ async execute(op) {
602
+ const { path, input } = op;
603
+ try {
604
+ if (this.queries[path]) {
605
+ const data = await this.executeQuery(path, input);
606
+ return { data };
607
+ }
608
+ if (this.mutations[path]) {
609
+ const data = await this.executeMutation(path, input);
610
+ return { data };
611
+ }
612
+ return { error: new Error(`Operation not found: ${path}`) };
613
+ } catch (error) {
614
+ return { error: error instanceof Error ? error : new Error(String(error)) };
615
+ }
616
+ }
617
+ buildOperationsMap() {
618
+ const result = {};
619
+ const setNested = (path, meta) => {
620
+ const parts = path.split(".");
621
+ let current = result;
622
+ for (let i = 0;i < parts.length - 1; i++) {
623
+ const part = parts[i];
624
+ if (!current[part] || "type" in current[part]) {
625
+ current[part] = {};
626
+ }
627
+ current = current[part];
628
+ }
629
+ current[parts[parts.length - 1]] = meta;
630
+ };
631
+ for (const [name, def] of Object.entries(this.queries)) {
632
+ setNested(name, { type: "query" });
633
+ }
634
+ for (const [name, def] of Object.entries(this.mutations)) {
635
+ const meta = { type: "mutation" };
636
+ if (def._optimistic) {
637
+ meta.optimistic = def._optimistic;
638
+ }
639
+ setNested(name, meta);
640
+ }
641
+ return result;
642
+ }
643
+ handleWebSocket(ws) {
644
+ const clientId = `client_${++this.connectionCounter}`;
645
+ const conn = {
646
+ id: clientId,
647
+ ws,
648
+ subscriptions: new Map
649
+ };
650
+ this.connections.set(clientId, conn);
651
+ this.stateManager.addClient({
652
+ id: clientId,
653
+ send: (msg) => {
654
+ ws.send(JSON.stringify(msg));
655
+ }
656
+ });
657
+ ws.onmessage = (event) => {
658
+ this.handleMessage(conn, event.data);
659
+ };
660
+ ws.onclose = () => {
661
+ this.handleDisconnect(conn);
662
+ };
663
+ }
664
+ handleMessage(conn, data) {
665
+ try {
666
+ const message = JSON.parse(data);
667
+ switch (message.type) {
668
+ case "handshake":
669
+ this.handleHandshake(conn, message);
670
+ break;
671
+ case "subscribe":
672
+ this.handleSubscribe(conn, message);
673
+ break;
674
+ case "updateFields":
675
+ this.handleUpdateFields(conn, message);
676
+ break;
677
+ case "unsubscribe":
678
+ this.handleUnsubscribe(conn, message);
679
+ break;
680
+ case "query":
681
+ this.handleQuery(conn, message);
682
+ break;
683
+ case "mutation":
684
+ this.handleMutation(conn, message);
685
+ break;
686
+ }
687
+ } catch (error) {
688
+ conn.ws.send(JSON.stringify({
689
+ type: "error",
690
+ error: { code: "PARSE_ERROR", message: String(error) }
691
+ }));
692
+ }
693
+ }
694
+ handleHandshake(conn, message) {
695
+ conn.ws.send(JSON.stringify({
696
+ type: "handshake",
697
+ id: message.id,
698
+ version: this.version,
699
+ operations: this.buildOperationsMap()
700
+ }));
701
+ }
702
+ async handleSubscribe(conn, message) {
703
+ const { id, operation, input, fields } = message;
704
+ const sub = {
705
+ id,
706
+ operation,
707
+ input,
708
+ fields,
709
+ entityKeys: new Set,
710
+ cleanups: [],
711
+ lastData: null
712
+ };
713
+ conn.subscriptions.set(id, sub);
714
+ try {
715
+ await this.executeSubscription(conn, sub);
716
+ } catch (error) {
717
+ conn.ws.send(JSON.stringify({
718
+ type: "error",
719
+ id,
720
+ error: { code: "EXECUTION_ERROR", message: String(error) }
721
+ }));
722
+ }
723
+ }
724
+ async executeSubscription(conn, sub) {
725
+ const queryDef = this.queries[sub.operation];
726
+ if (!queryDef) {
727
+ throw new Error(`Query not found: ${sub.operation}`);
728
+ }
729
+ if (queryDef._input && sub.input !== undefined) {
730
+ const result = queryDef._input.safeParse(sub.input);
731
+ if (!result.success) {
732
+ throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
733
+ }
734
+ }
735
+ const context = await this.contextFactory();
736
+ let isFirstUpdate = true;
737
+ const emitData = (data) => {
738
+ if (!data)
739
+ return;
740
+ const entityName = this.getEntityNameFromOutput(queryDef._output);
741
+ const entities = this.extractEntities(entityName, data);
742
+ for (const { entity, id, entityData } of entities) {
743
+ const entityKey = `${entity}:${id}`;
744
+ sub.entityKeys.add(entityKey);
745
+ this.stateManager.subscribe(conn.id, entity, id, sub.fields);
746
+ this.stateManager.emit(entity, id, entityData);
747
+ }
748
+ if (isFirstUpdate) {
749
+ conn.ws.send(JSON.stringify({
750
+ type: "data",
751
+ id: sub.id,
752
+ data
753
+ }));
754
+ isFirstUpdate = false;
755
+ sub.lastData = data;
756
+ } else {
757
+ const updates = this.computeUpdates(sub.lastData, data);
758
+ if (updates && Object.keys(updates).length > 0) {
759
+ conn.ws.send(JSON.stringify({
760
+ type: "update",
761
+ id: sub.id,
762
+ updates
763
+ }));
764
+ }
765
+ sub.lastData = data;
766
+ }
767
+ };
768
+ await runWithContext(this.ctx, context, async () => {
769
+ const resolver = queryDef._resolve;
770
+ if (!resolver) {
771
+ throw new Error(`Query ${sub.operation} has no resolver`);
772
+ }
773
+ const contextWithHelpers = {
774
+ ...context,
775
+ emit: emitData,
776
+ onCleanup: (fn) => {
777
+ sub.cleanups.push(fn);
778
+ return () => {
779
+ const idx = sub.cleanups.indexOf(fn);
780
+ if (idx >= 0)
781
+ sub.cleanups.splice(idx, 1);
782
+ };
783
+ }
784
+ };
785
+ const result = resolver({
786
+ input: sub.input,
787
+ ctx: contextWithHelpers
788
+ });
789
+ if (isAsyncIterable(result)) {
790
+ for await (const value of result) {
791
+ emitData(value);
792
+ }
793
+ } else {
794
+ const value = await result;
795
+ emitData(value);
796
+ }
797
+ });
798
+ }
799
+ handleUpdateFields(conn, message) {
800
+ const sub = conn.subscriptions.get(message.id);
801
+ if (!sub)
802
+ return;
803
+ if (message.addFields?.includes("*")) {
804
+ sub.fields = "*";
805
+ for (const entityKey of sub.entityKeys) {
806
+ const [entity, id] = entityKey.split(":");
807
+ this.stateManager.updateSubscription(conn.id, entity, id, "*");
808
+ }
809
+ return;
810
+ }
811
+ if (message.setFields !== undefined) {
812
+ sub.fields = message.setFields;
813
+ for (const entityKey of sub.entityKeys) {
814
+ const [entity, id] = entityKey.split(":");
815
+ this.stateManager.updateSubscription(conn.id, entity, id, sub.fields);
816
+ }
817
+ return;
818
+ }
819
+ if (sub.fields === "*") {
820
+ return;
821
+ }
822
+ const fields = new Set(sub.fields);
823
+ if (message.addFields) {
824
+ for (const field of message.addFields) {
825
+ fields.add(field);
826
+ }
827
+ }
828
+ if (message.removeFields) {
829
+ for (const field of message.removeFields) {
830
+ fields.delete(field);
831
+ }
832
+ }
833
+ sub.fields = Array.from(fields);
834
+ for (const entityKey of sub.entityKeys) {
835
+ const [entity, id] = entityKey.split(":");
836
+ this.stateManager.updateSubscription(conn.id, entity, id, sub.fields);
837
+ }
838
+ }
839
+ handleUnsubscribe(conn, message) {
840
+ const sub = conn.subscriptions.get(message.id);
841
+ if (!sub)
842
+ return;
843
+ for (const cleanup of sub.cleanups) {
844
+ try {
845
+ cleanup();
846
+ } catch (e) {
847
+ console.error("Cleanup error:", e);
848
+ }
849
+ }
850
+ for (const entityKey of sub.entityKeys) {
851
+ const [entity, id] = entityKey.split(":");
852
+ this.stateManager.unsubscribe(conn.id, entity, id);
853
+ }
854
+ conn.subscriptions.delete(message.id);
855
+ }
856
+ async handleQuery(conn, message) {
857
+ try {
858
+ let input = message.input;
859
+ if (message.select) {
860
+ input = { ...message.input || {}, $select: message.select };
861
+ }
862
+ const result = await this.executeQuery(message.operation, input);
863
+ const selected = message.fields && !message.select ? this.applySelection(result, message.fields) : result;
864
+ conn.ws.send(JSON.stringify({
865
+ type: "result",
866
+ id: message.id,
867
+ data: selected
868
+ }));
869
+ } catch (error) {
870
+ conn.ws.send(JSON.stringify({
871
+ type: "error",
872
+ id: message.id,
873
+ error: { code: "EXECUTION_ERROR", message: String(error) }
874
+ }));
875
+ }
876
+ }
877
+ async handleMutation(conn, message) {
878
+ try {
879
+ const result = await this.executeMutation(message.operation, message.input);
880
+ const entityName = this.getEntityNameFromMutation(message.operation);
881
+ const entities = this.extractEntities(entityName, result);
882
+ for (const { entity, id, entityData } of entities) {
883
+ this.stateManager.emit(entity, id, entityData);
884
+ }
885
+ conn.ws.send(JSON.stringify({
886
+ type: "result",
887
+ id: message.id,
888
+ data: result
889
+ }));
890
+ } catch (error) {
891
+ conn.ws.send(JSON.stringify({
892
+ type: "error",
893
+ id: message.id,
894
+ error: { code: "EXECUTION_ERROR", message: String(error) }
895
+ }));
896
+ }
897
+ }
898
+ handleDisconnect(conn) {
899
+ for (const sub of conn.subscriptions.values()) {
900
+ for (const cleanup of sub.cleanups) {
901
+ try {
902
+ cleanup();
903
+ } catch (e) {
904
+ console.error("Cleanup error:", e);
905
+ }
906
+ }
907
+ }
908
+ this.stateManager.removeClient(conn.id);
909
+ this.connections.delete(conn.id);
910
+ }
911
+ async executeQuery(name, input) {
912
+ const queryDef = this.queries[name];
913
+ if (!queryDef) {
914
+ throw new Error(`Query not found: ${name}`);
915
+ }
916
+ let select;
917
+ let cleanInput = input;
918
+ if (input && typeof input === "object" && "$select" in input) {
919
+ const { $select, ...rest } = input;
920
+ select = $select;
921
+ cleanInput = Object.keys(rest).length > 0 ? rest : undefined;
922
+ }
923
+ if (queryDef._input && cleanInput !== undefined) {
924
+ const result = queryDef._input.safeParse(cleanInput);
925
+ if (!result.success) {
926
+ throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
927
+ }
928
+ }
929
+ const context = await this.contextFactory();
930
+ try {
931
+ return await runWithContext(this.ctx, context, async () => {
932
+ const resolver = queryDef._resolve;
933
+ if (!resolver) {
934
+ throw new Error(`Query ${name} has no resolver`);
935
+ }
936
+ const resolverCtx = {
937
+ input: cleanInput,
938
+ ctx: context,
939
+ emit: () => {},
940
+ onCleanup: () => () => {}
941
+ };
942
+ const result = resolver(resolverCtx);
943
+ let data;
944
+ if (isAsyncIterable(result)) {
945
+ for await (const value of result) {
946
+ data = value;
947
+ break;
948
+ }
949
+ if (data === undefined) {
950
+ throw new Error(`Query ${name} returned empty stream`);
951
+ }
952
+ } else {
953
+ data = await result;
954
+ }
955
+ return this.processQueryResult(name, data, select);
956
+ });
957
+ } finally {
958
+ this.clearLoaders();
959
+ }
960
+ }
961
+ async executeMutation(name, input) {
962
+ const mutationDef = this.mutations[name];
963
+ if (!mutationDef) {
964
+ throw new Error(`Mutation not found: ${name}`);
965
+ }
966
+ if (mutationDef._input) {
967
+ const result = mutationDef._input.safeParse(input);
968
+ if (!result.success) {
969
+ throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
970
+ }
971
+ }
972
+ const context = await this.contextFactory();
973
+ try {
974
+ return await runWithContext(this.ctx, context, async () => {
975
+ const resolver = mutationDef._resolve;
976
+ if (!resolver) {
977
+ throw new Error(`Mutation ${name} has no resolver`);
978
+ }
979
+ const result = await resolver({
980
+ input,
981
+ ctx: context
982
+ });
983
+ const entityName = this.getEntityNameFromMutation(name);
984
+ const entities = this.extractEntities(entityName, result);
985
+ for (const { entity, id, entityData } of entities) {
986
+ this.stateManager.emit(entity, id, entityData);
987
+ }
988
+ return result;
989
+ });
990
+ } finally {
991
+ this.clearLoaders();
992
+ }
993
+ }
994
+ async handleRequest(req) {
995
+ const url = new URL(req.url);
996
+ if (req.method === "GET" && url.pathname.endsWith("/__lens/metadata")) {
997
+ return new Response(JSON.stringify(this.getMetadata()), {
998
+ headers: { "Content-Type": "application/json" }
999
+ });
1000
+ }
1001
+ if (req.method === "POST") {
1002
+ try {
1003
+ const body = await req.json();
1004
+ if (this.queries[body.operation]) {
1005
+ const result = await this.executeQuery(body.operation, body.input);
1006
+ return new Response(JSON.stringify({ data: result }), {
1007
+ headers: { "Content-Type": "application/json" }
1008
+ });
1009
+ }
1010
+ if (this.mutations[body.operation]) {
1011
+ const result = await this.executeMutation(body.operation, body.input);
1012
+ return new Response(JSON.stringify({ data: result }), {
1013
+ headers: { "Content-Type": "application/json" }
1014
+ });
1015
+ }
1016
+ return new Response(JSON.stringify({ error: `Operation not found: ${body.operation}` }), {
1017
+ status: 404,
1018
+ headers: { "Content-Type": "application/json" }
1019
+ });
1020
+ } catch (error) {
1021
+ return new Response(JSON.stringify({ error: String(error) }), {
1022
+ status: 500,
1023
+ headers: { "Content-Type": "application/json" }
1024
+ });
1025
+ }
1026
+ }
1027
+ return new Response("Method not allowed", { status: 405 });
1028
+ }
1029
+ async listen(port) {
1030
+ this.server = Bun.serve({
1031
+ port,
1032
+ fetch: (req, server) => {
1033
+ if (server.upgrade(req)) {
1034
+ return;
1035
+ }
1036
+ return this.handleRequest(req);
1037
+ },
1038
+ websocket: {
1039
+ message: (ws, message) => {
1040
+ const conn = this.findConnectionByWs(ws);
1041
+ if (conn) {
1042
+ this.handleMessage(conn, String(message));
1043
+ }
1044
+ },
1045
+ close: (ws) => {
1046
+ const conn = this.findConnectionByWs(ws);
1047
+ if (conn) {
1048
+ this.handleDisconnect(conn);
1049
+ }
1050
+ }
1051
+ }
1052
+ });
1053
+ console.log(`Lens server listening on port ${port}`);
1054
+ }
1055
+ async close() {
1056
+ if (this.server && typeof this.server.stop === "function") {
1057
+ this.server.stop();
1058
+ }
1059
+ this.server = null;
1060
+ }
1061
+ findConnectionByWs(ws) {
1062
+ for (const conn of this.connections.values()) {
1063
+ if (conn.ws === ws) {
1064
+ return conn;
1065
+ }
1066
+ }
1067
+ return;
1068
+ }
1069
+ getEntityNameFromOutput(output) {
1070
+ if (!output)
1071
+ return "unknown";
1072
+ if (typeof output === "object" && output !== null) {
1073
+ if ("_name" in output) {
1074
+ return output._name;
1075
+ }
1076
+ if ("name" in output) {
1077
+ return output.name;
1078
+ }
1079
+ }
1080
+ if (Array.isArray(output) && output.length > 0) {
1081
+ const first = output[0];
1082
+ if (typeof first === "object" && first !== null) {
1083
+ if ("_name" in first) {
1084
+ return first._name;
1085
+ }
1086
+ if ("name" in first) {
1087
+ return first.name;
1088
+ }
1089
+ }
1090
+ }
1091
+ return "unknown";
1092
+ }
1093
+ getEntityNameFromMutation(name) {
1094
+ const mutationDef = this.mutations[name];
1095
+ if (!mutationDef)
1096
+ return "unknown";
1097
+ return this.getEntityNameFromOutput(mutationDef._output);
1098
+ }
1099
+ extractEntities(entityName, data) {
1100
+ const results = [];
1101
+ if (!data)
1102
+ return results;
1103
+ if (Array.isArray(data)) {
1104
+ for (const item of data) {
1105
+ if (item && typeof item === "object" && "id" in item) {
1106
+ results.push({
1107
+ entity: entityName,
1108
+ id: String(item.id),
1109
+ entityData: item
1110
+ });
1111
+ }
1112
+ }
1113
+ } else if (typeof data === "object" && "id" in data) {
1114
+ results.push({
1115
+ entity: entityName,
1116
+ id: String(data.id),
1117
+ entityData: data
1118
+ });
1119
+ }
1120
+ return results;
1121
+ }
1122
+ applySelection(data, fields) {
1123
+ if (fields === "*" || !data)
1124
+ return data;
1125
+ if (Array.isArray(data)) {
1126
+ return data.map((item) => this.applySelectionToObject(item, fields));
1127
+ }
1128
+ return this.applySelectionToObject(data, fields);
1129
+ }
1130
+ applySelectionToObject(data, fields) {
1131
+ if (!data || typeof data !== "object")
1132
+ return null;
1133
+ const result = {};
1134
+ const obj = data;
1135
+ if ("id" in obj) {
1136
+ result.id = obj.id;
1137
+ }
1138
+ if (Array.isArray(fields)) {
1139
+ for (const field of fields) {
1140
+ if (field in obj) {
1141
+ result[field] = obj[field];
1142
+ }
1143
+ }
1144
+ return result;
1145
+ }
1146
+ for (const [key, value] of Object.entries(fields)) {
1147
+ if (value === false)
1148
+ continue;
1149
+ const dataValue = obj[key];
1150
+ if (value === true) {
1151
+ result[key] = dataValue;
1152
+ } else if (typeof value === "object" && value !== null) {
1153
+ const nestedSelect = value.select ?? value;
1154
+ if (Array.isArray(dataValue)) {
1155
+ result[key] = dataValue.map((item) => this.applySelectionToObject(item, nestedSelect));
1156
+ } else if (dataValue !== null && typeof dataValue === "object") {
1157
+ result[key] = this.applySelectionToObject(dataValue, nestedSelect);
1158
+ } else {
1159
+ result[key] = dataValue;
1160
+ }
1161
+ }
1162
+ }
1163
+ return result;
1164
+ }
1165
+ async executeEntityResolvers(entityName, data, select) {
1166
+ if (!data || !select || !this.resolvers)
1167
+ return data;
1168
+ const result = { ...data };
1169
+ for (const [fieldName, fieldSelect] of Object.entries(select)) {
1170
+ if (fieldSelect === false || fieldSelect === true)
1171
+ continue;
1172
+ const resolver = this.resolvers.getResolver(entityName, fieldName);
1173
+ if (!resolver)
1174
+ continue;
1175
+ if (isBatchResolver(resolver)) {
1176
+ const loaderKey = `${entityName}.${fieldName}`;
1177
+ if (!this.loaders.has(loaderKey)) {
1178
+ this.loaders.set(loaderKey, new DataLoader(async (parents) => {
1179
+ return resolver.batch(parents);
1180
+ }));
1181
+ }
1182
+ const loader = this.loaders.get(loaderKey);
1183
+ result[fieldName] = await loader.load(data);
1184
+ } else {
1185
+ result[fieldName] = await resolver(data);
1186
+ }
1187
+ const nestedSelect = fieldSelect.select;
1188
+ if (nestedSelect && result[fieldName]) {
1189
+ const relationData = result[fieldName];
1190
+ const targetEntity = this.getRelationTargetEntity(entityName, fieldName);
1191
+ if (Array.isArray(relationData)) {
1192
+ result[fieldName] = await Promise.all(relationData.map((item) => this.executeEntityResolvers(targetEntity, item, nestedSelect)));
1193
+ } else {
1194
+ result[fieldName] = await this.executeEntityResolvers(targetEntity, relationData, nestedSelect);
1195
+ }
1196
+ }
1197
+ }
1198
+ return result;
1199
+ }
1200
+ getRelationTargetEntity(entityName, fieldName) {
1201
+ const entityDef = this.entities[entityName];
1202
+ if (!entityDef)
1203
+ return fieldName;
1204
+ const fields = entityDef.fields;
1205
+ if (!fields)
1206
+ return fieldName;
1207
+ const fieldDef = fields[fieldName];
1208
+ if (!fieldDef)
1209
+ return fieldName;
1210
+ if (fieldDef._type === "hasMany" || fieldDef._type === "hasOne" || fieldDef._type === "belongsTo") {
1211
+ return fieldDef._target ?? fieldName;
1212
+ }
1213
+ return fieldName;
1214
+ }
1215
+ serializeEntity(entityName, data) {
1216
+ if (data === null)
1217
+ return null;
1218
+ const entityDef = this.entities[entityName];
1219
+ if (!entityDef)
1220
+ return data;
1221
+ const fields = entityDef.fields;
1222
+ if (!fields)
1223
+ return data;
1224
+ const result = {};
1225
+ for (const [fieldName, value] of Object.entries(data)) {
1226
+ const fieldType = fields[fieldName];
1227
+ if (!fieldType) {
1228
+ result[fieldName] = value;
1229
+ continue;
1230
+ }
1231
+ if (value === null || value === undefined) {
1232
+ result[fieldName] = value;
1233
+ continue;
1234
+ }
1235
+ if (fieldType._type === "hasMany" || fieldType._type === "belongsTo" || fieldType._type === "hasOne") {
1236
+ const targetEntity = fieldType._target;
1237
+ if (targetEntity && Array.isArray(value)) {
1238
+ result[fieldName] = value.map((item) => this.serializeEntity(targetEntity, item));
1239
+ } else if (targetEntity && typeof value === "object") {
1240
+ result[fieldName] = this.serializeEntity(targetEntity, value);
1241
+ } else {
1242
+ result[fieldName] = value;
1243
+ }
1244
+ continue;
1245
+ }
1246
+ if (typeof fieldType.serialize === "function") {
1247
+ try {
1248
+ result[fieldName] = fieldType.serialize(value);
1249
+ } catch (error) {
1250
+ console.warn(`Failed to serialize field ${entityName}.${fieldName}:`, error);
1251
+ result[fieldName] = value;
1252
+ }
1253
+ } else {
1254
+ result[fieldName] = value;
1255
+ }
1256
+ }
1257
+ return result;
1258
+ }
1259
+ async processQueryResult(queryName, data, select) {
1260
+ if (data === null || data === undefined)
1261
+ return data;
1262
+ const queryDef = this.queries[queryName];
1263
+ const entityName = this.getEntityNameFromOutput(queryDef?._output);
1264
+ if (Array.isArray(data)) {
1265
+ const processedItems = await Promise.all(data.map(async (item) => {
1266
+ let result2 = item;
1267
+ if (select && this.resolvers) {
1268
+ result2 = await this.executeEntityResolvers(entityName, item, select);
1269
+ }
1270
+ if (select) {
1271
+ result2 = this.applySelection(result2, select);
1272
+ }
1273
+ if (entityName) {
1274
+ return this.serializeEntity(entityName, result2);
1275
+ }
1276
+ return result2;
1277
+ }));
1278
+ return processedItems;
1279
+ }
1280
+ let result = data;
1281
+ if (select && this.resolvers) {
1282
+ result = await this.executeEntityResolvers(entityName, data, select);
1283
+ }
1284
+ if (select) {
1285
+ result = this.applySelection(result, select);
1286
+ }
1287
+ if (entityName && typeof result === "object" && result !== null) {
1288
+ return this.serializeEntity(entityName, result);
1289
+ }
1290
+ return result;
1291
+ }
1292
+ computeUpdates(oldData, newData) {
1293
+ if (!oldData || !newData)
1294
+ return null;
1295
+ if (typeof oldData !== "object" || typeof newData !== "object")
1296
+ return null;
1297
+ const updates = {};
1298
+ const oldObj = oldData;
1299
+ const newObj = newData;
1300
+ for (const key of Object.keys(newObj)) {
1301
+ const oldValue = oldObj[key];
1302
+ const newValue = newObj[key];
1303
+ if (!this.deepEqual(oldValue, newValue)) {
1304
+ updates[key] = createUpdate(oldValue, newValue);
1305
+ }
1306
+ }
1307
+ return Object.keys(updates).length > 0 ? updates : null;
1308
+ }
1309
+ deepEqual(a, b) {
1310
+ if (a === b)
1311
+ return true;
1312
+ if (typeof a !== typeof b)
1313
+ return false;
1314
+ if (typeof a !== "object" || a === null || b === null)
1315
+ return false;
1316
+ const aObj = a;
1317
+ const bObj = b;
1318
+ const aKeys = Object.keys(aObj);
1319
+ const bKeys = Object.keys(bObj);
1320
+ if (aKeys.length !== bKeys.length)
1321
+ return false;
1322
+ for (const key of aKeys) {
1323
+ if (!this.deepEqual(aObj[key], bObj[key]))
1324
+ return false;
1325
+ }
1326
+ return true;
1327
+ }
1328
+ clearLoaders() {
1329
+ for (const loader of this.loaders.values()) {
1330
+ loader.clear();
1331
+ }
1332
+ this.loaders.clear();
1333
+ }
1334
+ }
1335
+ function isAsyncIterable(value) {
1336
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
1337
+ }
1338
+ function createServer(config) {
1339
+ const server = new LensServerImpl(config);
1340
+ return server;
1341
+ }
1342
+ // src/sse/handler.ts
1343
+ class SSEHandler {
1344
+ stateManager;
1345
+ heartbeatInterval;
1346
+ clients = new Map;
1347
+ clientCounter = 0;
1348
+ constructor(config) {
1349
+ this.stateManager = config.stateManager;
1350
+ this.heartbeatInterval = config.heartbeatInterval ?? 30000;
1351
+ }
1352
+ handleConnection(req) {
1353
+ const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
1354
+ const encoder = new TextEncoder;
1355
+ const stream = new ReadableStream({
1356
+ start: (controller) => {
1357
+ const stateClient = {
1358
+ id: clientId,
1359
+ send: (msg) => {
1360
+ try {
1361
+ const data = `data: ${JSON.stringify(msg)}
1362
+
1363
+ `;
1364
+ controller.enqueue(encoder.encode(data));
1365
+ } catch {
1366
+ this.removeClient(clientId);
1367
+ }
1368
+ }
1369
+ };
1370
+ this.stateManager.addClient(stateClient);
1371
+ controller.enqueue(encoder.encode(`event: connected
1372
+ data: ${JSON.stringify({ clientId })}
1373
+
1374
+ `));
1375
+ const heartbeat = setInterval(() => {
1376
+ try {
1377
+ controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}
1378
+
1379
+ `));
1380
+ } catch {
1381
+ this.removeClient(clientId);
1382
+ }
1383
+ }, this.heartbeatInterval);
1384
+ this.clients.set(clientId, { controller, heartbeat });
1385
+ },
1386
+ cancel: () => {
1387
+ this.removeClient(clientId);
1388
+ }
1389
+ });
1390
+ return new Response(stream, {
1391
+ headers: {
1392
+ "Content-Type": "text/event-stream",
1393
+ "Cache-Control": "no-cache",
1394
+ Connection: "keep-alive",
1395
+ "Access-Control-Allow-Origin": "*"
1396
+ }
1397
+ });
1398
+ }
1399
+ removeClient(clientId) {
1400
+ const client = this.clients.get(clientId);
1401
+ if (client) {
1402
+ clearInterval(client.heartbeat);
1403
+ this.clients.delete(clientId);
1404
+ }
1405
+ this.stateManager.removeClient(clientId);
1406
+ }
1407
+ closeClient(clientId) {
1408
+ const client = this.clients.get(clientId);
1409
+ if (client) {
1410
+ try {
1411
+ client.controller.close();
1412
+ } catch {}
1413
+ this.removeClient(clientId);
1414
+ }
1415
+ }
1416
+ getClientCount() {
1417
+ return this.clients.size;
1418
+ }
1419
+ getClientIds() {
1420
+ return Array.from(this.clients.keys());
1421
+ }
1422
+ closeAll() {
1423
+ for (const clientId of this.clients.keys()) {
1424
+ this.closeClient(clientId);
1425
+ }
1426
+ }
1427
+ }
1428
+ function createSSEHandler(config) {
1429
+ return new SSEHandler(config);
1430
+ }
1431
+ export {
1432
+ createServer,
1433
+ createSSEHandler,
1434
+ createGraphStateManager,
1435
+ SSEHandler,
1436
+ GraphStateManager
1437
+ };