@sylphx/lens-server 1.0.3 → 1.2.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/dist/index.d.ts +509 -9
- package/dist/index.js +269 -295
- package/package.json +3 -4
- package/src/e2e/server.test.ts +10 -10
- package/src/server/create.test.ts +11 -11
- package/src/server/create.ts +46 -20
- package/src/state/graph-state-manager.test.ts +215 -0
- package/src/state/graph-state-manager.ts +423 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server/create.d.ts +0 -226
- package/dist/server/create.d.ts.map +0 -1
- package/dist/sse/handler.d.ts +0 -78
- package/dist/sse/handler.d.ts.map +0 -1
- package/dist/state/graph-state-manager.d.ts +0 -146
- package/dist/state/graph-state-manager.d.ts.map +0 -1
- package/dist/state/index.d.ts +0 -7
- package/dist/state/index.d.ts.map +0 -1
|
@@ -9,7 +9,16 @@
|
|
|
9
9
|
* - Pushes updates to subscribed clients
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
type EntityKey,
|
|
14
|
+
type Update,
|
|
15
|
+
type EmitCommand,
|
|
16
|
+
type InternalFieldUpdate,
|
|
17
|
+
type ArrayOperation,
|
|
18
|
+
createUpdate,
|
|
19
|
+
applyUpdate,
|
|
20
|
+
makeEntityKey,
|
|
21
|
+
} from "@sylphx/lens-core";
|
|
13
22
|
|
|
14
23
|
// Re-export for convenience
|
|
15
24
|
export type { EntityKey };
|
|
@@ -51,6 +60,12 @@ interface ClientEntityState {
|
|
|
51
60
|
fields: Set<string> | "*";
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
/** Per-client state for an array */
|
|
64
|
+
interface ClientArrayState {
|
|
65
|
+
/** Last array state sent to this client */
|
|
66
|
+
lastState: unknown[];
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/** Configuration */
|
|
55
70
|
export interface GraphStateManagerConfig {
|
|
56
71
|
/** Called when an entity has no more subscribers */
|
|
@@ -89,9 +104,15 @@ export class GraphStateManager {
|
|
|
89
104
|
/** Canonical state per entity (server truth) */
|
|
90
105
|
private canonical = new Map<EntityKey, Record<string, unknown>>();
|
|
91
106
|
|
|
107
|
+
/** Canonical array state per entity (server truth for array outputs) */
|
|
108
|
+
private canonicalArrays = new Map<EntityKey, unknown[]>();
|
|
109
|
+
|
|
92
110
|
/** Per-client state tracking */
|
|
93
111
|
private clientStates = new Map<string, Map<EntityKey, ClientEntityState>>();
|
|
94
112
|
|
|
113
|
+
/** Per-client array state tracking */
|
|
114
|
+
private clientArrayStates = new Map<string, Map<EntityKey, ClientArrayState>>();
|
|
115
|
+
|
|
95
116
|
/** Entity → subscribed client IDs */
|
|
96
117
|
private entitySubscribers = new Map<EntityKey, Set<string>>();
|
|
97
118
|
|
|
@@ -112,6 +133,7 @@ export class GraphStateManager {
|
|
|
112
133
|
addClient(client: StateClient): void {
|
|
113
134
|
this.clients.set(client.id, client);
|
|
114
135
|
this.clientStates.set(client.id, new Map());
|
|
136
|
+
this.clientArrayStates.set(client.id, new Map());
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
/**
|
|
@@ -128,6 +150,7 @@ export class GraphStateManager {
|
|
|
128
150
|
|
|
129
151
|
this.clients.delete(clientId);
|
|
130
152
|
this.clientStates.delete(clientId);
|
|
153
|
+
this.clientArrayStates.delete(clientId);
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
// ===========================================================================
|
|
@@ -245,6 +268,278 @@ export class GraphStateManager {
|
|
|
245
268
|
}
|
|
246
269
|
}
|
|
247
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Emit a field-level update with a specific strategy.
|
|
273
|
+
* Applies the update to canonical state and pushes to clients.
|
|
274
|
+
*
|
|
275
|
+
* @param entity - Entity name
|
|
276
|
+
* @param id - Entity ID
|
|
277
|
+
* @param field - Field name to update
|
|
278
|
+
* @param update - Update with strategy (value/delta/patch)
|
|
279
|
+
*/
|
|
280
|
+
emitField(entity: string, id: string, field: string, update: Update): void {
|
|
281
|
+
const key = this.makeKey(entity, id);
|
|
282
|
+
|
|
283
|
+
// Get or create canonical state
|
|
284
|
+
let currentCanonical = this.canonical.get(key);
|
|
285
|
+
if (!currentCanonical) {
|
|
286
|
+
currentCanonical = {};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Apply update to canonical state based on strategy
|
|
290
|
+
const oldValue = currentCanonical[field];
|
|
291
|
+
const newValue = applyUpdate(oldValue, update);
|
|
292
|
+
currentCanonical = { ...currentCanonical, [field]: newValue };
|
|
293
|
+
|
|
294
|
+
this.canonical.set(key, currentCanonical);
|
|
295
|
+
|
|
296
|
+
// Push updates to all subscribed clients
|
|
297
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
298
|
+
if (!subscribers) return;
|
|
299
|
+
|
|
300
|
+
for (const clientId of subscribers) {
|
|
301
|
+
this.pushFieldToClient(clientId, entity, id, key, field, newValue);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Emit multiple field updates in a batch.
|
|
307
|
+
* More efficient than multiple emitField calls.
|
|
308
|
+
*
|
|
309
|
+
* @param entity - Entity name
|
|
310
|
+
* @param id - Entity ID
|
|
311
|
+
* @param updates - Array of field updates
|
|
312
|
+
*/
|
|
313
|
+
emitBatch(entity: string, id: string, updates: InternalFieldUpdate[]): void {
|
|
314
|
+
const key = this.makeKey(entity, id);
|
|
315
|
+
|
|
316
|
+
// Get or create canonical state
|
|
317
|
+
let currentCanonical = this.canonical.get(key);
|
|
318
|
+
if (!currentCanonical) {
|
|
319
|
+
currentCanonical = {};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Apply all updates to canonical state
|
|
323
|
+
const changedFields: string[] = [];
|
|
324
|
+
for (const { field, update } of updates) {
|
|
325
|
+
const oldValue = currentCanonical[field];
|
|
326
|
+
const newValue = applyUpdate(oldValue, update);
|
|
327
|
+
currentCanonical[field] = newValue;
|
|
328
|
+
changedFields.push(field);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.canonical.set(key, currentCanonical);
|
|
332
|
+
|
|
333
|
+
// Push updates to all subscribed clients
|
|
334
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
335
|
+
if (!subscribers) return;
|
|
336
|
+
|
|
337
|
+
for (const clientId of subscribers) {
|
|
338
|
+
this.pushFieldsToClient(clientId, entity, id, key, changedFields, currentCanonical);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Process an EmitCommand from the Emit API.
|
|
344
|
+
* Routes to appropriate emit method.
|
|
345
|
+
*
|
|
346
|
+
* @param entity - Entity name
|
|
347
|
+
* @param id - Entity ID
|
|
348
|
+
* @param command - Emit command from resolver
|
|
349
|
+
*/
|
|
350
|
+
processCommand(entity: string, id: string, command: EmitCommand): void {
|
|
351
|
+
switch (command.type) {
|
|
352
|
+
case "full":
|
|
353
|
+
this.emit(entity, id, command.data as Record<string, unknown>, {
|
|
354
|
+
replace: command.replace,
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
case "field":
|
|
358
|
+
this.emitField(entity, id, command.field, command.update);
|
|
359
|
+
break;
|
|
360
|
+
case "batch":
|
|
361
|
+
this.emitBatch(entity, id, command.updates);
|
|
362
|
+
break;
|
|
363
|
+
case "array":
|
|
364
|
+
this.emitArrayOperation(entity, id, command.operation);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ===========================================================================
|
|
370
|
+
// Array State Emission
|
|
371
|
+
// ===========================================================================
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Emit array data (replace entire array).
|
|
375
|
+
*
|
|
376
|
+
* @param entity - Entity name
|
|
377
|
+
* @param id - Entity ID
|
|
378
|
+
* @param items - Array items
|
|
379
|
+
*/
|
|
380
|
+
emitArray(entity: string, id: string, items: unknown[]): void {
|
|
381
|
+
const key = this.makeKey(entity, id);
|
|
382
|
+
this.canonicalArrays.set(key, [...items]);
|
|
383
|
+
|
|
384
|
+
// Push updates to all subscribed clients
|
|
385
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
386
|
+
if (!subscribers) return;
|
|
387
|
+
|
|
388
|
+
for (const clientId of subscribers) {
|
|
389
|
+
this.pushArrayToClient(clientId, entity, id, key, items);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Apply an array operation to the canonical state.
|
|
395
|
+
*
|
|
396
|
+
* @param entity - Entity name
|
|
397
|
+
* @param id - Entity ID
|
|
398
|
+
* @param operation - Array operation to apply
|
|
399
|
+
*/
|
|
400
|
+
emitArrayOperation(entity: string, id: string, operation: ArrayOperation): void {
|
|
401
|
+
const key = this.makeKey(entity, id);
|
|
402
|
+
|
|
403
|
+
// Get or create canonical array state
|
|
404
|
+
let currentArray = this.canonicalArrays.get(key);
|
|
405
|
+
if (!currentArray) {
|
|
406
|
+
currentArray = [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Apply operation
|
|
410
|
+
const newArray = this.applyArrayOperation([...currentArray], operation);
|
|
411
|
+
this.canonicalArrays.set(key, newArray);
|
|
412
|
+
|
|
413
|
+
// Push updates to all subscribed clients
|
|
414
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
415
|
+
if (!subscribers) return;
|
|
416
|
+
|
|
417
|
+
for (const clientId of subscribers) {
|
|
418
|
+
this.pushArrayToClient(clientId, entity, id, key, newArray);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Apply an array operation and return new array.
|
|
424
|
+
*/
|
|
425
|
+
private applyArrayOperation(array: unknown[], operation: ArrayOperation): unknown[] {
|
|
426
|
+
switch (operation.op) {
|
|
427
|
+
case "push":
|
|
428
|
+
return [...array, operation.item];
|
|
429
|
+
|
|
430
|
+
case "unshift":
|
|
431
|
+
return [operation.item, ...array];
|
|
432
|
+
|
|
433
|
+
case "insert":
|
|
434
|
+
return [
|
|
435
|
+
...array.slice(0, operation.index),
|
|
436
|
+
operation.item,
|
|
437
|
+
...array.slice(operation.index),
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
case "remove":
|
|
441
|
+
return [...array.slice(0, operation.index), ...array.slice(operation.index + 1)];
|
|
442
|
+
|
|
443
|
+
case "removeById": {
|
|
444
|
+
const idx = array.findIndex(
|
|
445
|
+
(item) =>
|
|
446
|
+
typeof item === "object" &&
|
|
447
|
+
item !== null &&
|
|
448
|
+
"id" in item &&
|
|
449
|
+
(item as { id: string }).id === operation.id,
|
|
450
|
+
);
|
|
451
|
+
if (idx === -1) return array;
|
|
452
|
+
return [...array.slice(0, idx), ...array.slice(idx + 1)];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case "update":
|
|
456
|
+
return array.map((item, i) => (i === operation.index ? operation.item : item));
|
|
457
|
+
|
|
458
|
+
case "updateById":
|
|
459
|
+
return array.map((item) =>
|
|
460
|
+
typeof item === "object" &&
|
|
461
|
+
item !== null &&
|
|
462
|
+
"id" in item &&
|
|
463
|
+
(item as { id: string }).id === operation.id
|
|
464
|
+
? operation.item
|
|
465
|
+
: item,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
case "merge":
|
|
469
|
+
return array.map((item, i) =>
|
|
470
|
+
i === operation.index && typeof item === "object" && item !== null
|
|
471
|
+
? { ...item, ...(operation.partial as object) }
|
|
472
|
+
: item,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
case "mergeById":
|
|
476
|
+
return array.map((item) =>
|
|
477
|
+
typeof item === "object" &&
|
|
478
|
+
item !== null &&
|
|
479
|
+
"id" in item &&
|
|
480
|
+
(item as { id: string }).id === operation.id
|
|
481
|
+
? { ...item, ...(operation.partial as object) }
|
|
482
|
+
: item,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
default:
|
|
486
|
+
return array;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Push array update to a specific client.
|
|
492
|
+
* Computes optimal diff strategy.
|
|
493
|
+
*/
|
|
494
|
+
private pushArrayToClient(
|
|
495
|
+
clientId: string,
|
|
496
|
+
entity: string,
|
|
497
|
+
id: string,
|
|
498
|
+
key: EntityKey,
|
|
499
|
+
newArray: unknown[],
|
|
500
|
+
): void {
|
|
501
|
+
const client = this.clients.get(clientId);
|
|
502
|
+
if (!client) return;
|
|
503
|
+
|
|
504
|
+
const clientArrayStateMap = this.clientArrayStates.get(clientId);
|
|
505
|
+
if (!clientArrayStateMap) return;
|
|
506
|
+
|
|
507
|
+
let clientArrayState = clientArrayStateMap.get(key);
|
|
508
|
+
if (!clientArrayState) {
|
|
509
|
+
// Initialize client array state
|
|
510
|
+
clientArrayState = { lastState: [] };
|
|
511
|
+
clientArrayStateMap.set(key, clientArrayState);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const { lastState } = clientArrayState;
|
|
515
|
+
|
|
516
|
+
// Skip if unchanged
|
|
517
|
+
if (JSON.stringify(lastState) === JSON.stringify(newArray)) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// For now, send full array replacement
|
|
522
|
+
// TODO: Compute array diff for optimal transfer
|
|
523
|
+
client.send({
|
|
524
|
+
type: "update",
|
|
525
|
+
entity,
|
|
526
|
+
id,
|
|
527
|
+
updates: {
|
|
528
|
+
_items: { strategy: "value", data: newArray },
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Update client's last known state
|
|
533
|
+
clientArrayState.lastState = [...newArray];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get current canonical array state
|
|
538
|
+
*/
|
|
539
|
+
getArrayState(entity: string, id: string): unknown[] | undefined {
|
|
540
|
+
return this.canonicalArrays.get(this.makeKey(entity, id));
|
|
541
|
+
}
|
|
542
|
+
|
|
248
543
|
/**
|
|
249
544
|
* Get current canonical state for an entity
|
|
250
545
|
*/
|
|
@@ -330,6 +625,131 @@ export class GraphStateManager {
|
|
|
330
625
|
}
|
|
331
626
|
}
|
|
332
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Push a single field update to a client.
|
|
630
|
+
* Computes optimal transfer strategy.
|
|
631
|
+
*/
|
|
632
|
+
private pushFieldToClient(
|
|
633
|
+
clientId: string,
|
|
634
|
+
entity: string,
|
|
635
|
+
id: string,
|
|
636
|
+
key: EntityKey,
|
|
637
|
+
field: string,
|
|
638
|
+
newValue: unknown,
|
|
639
|
+
): void {
|
|
640
|
+
const client = this.clients.get(clientId);
|
|
641
|
+
if (!client) return;
|
|
642
|
+
|
|
643
|
+
const clientStateMap = this.clientStates.get(clientId);
|
|
644
|
+
if (!clientStateMap) return;
|
|
645
|
+
|
|
646
|
+
const clientEntityState = clientStateMap.get(key);
|
|
647
|
+
if (!clientEntityState) return;
|
|
648
|
+
|
|
649
|
+
const { lastState, fields } = clientEntityState;
|
|
650
|
+
|
|
651
|
+
// Check if client is subscribed to this field
|
|
652
|
+
if (fields !== "*" && !fields.has(field)) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const oldValue = lastState[field];
|
|
657
|
+
|
|
658
|
+
// Skip if unchanged
|
|
659
|
+
if (oldValue === newValue) return;
|
|
660
|
+
if (
|
|
661
|
+
typeof oldValue === "object" &&
|
|
662
|
+
typeof newValue === "object" &&
|
|
663
|
+
JSON.stringify(oldValue) === JSON.stringify(newValue)
|
|
664
|
+
) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Compute optimal update for transfer
|
|
669
|
+
const update = createUpdate(oldValue, newValue);
|
|
670
|
+
|
|
671
|
+
// Send update
|
|
672
|
+
client.send({
|
|
673
|
+
type: "update",
|
|
674
|
+
entity,
|
|
675
|
+
id,
|
|
676
|
+
updates: { [field]: update },
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Update client's last known state
|
|
680
|
+
clientEntityState.lastState[field] = newValue;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Push multiple field updates to a client.
|
|
685
|
+
* Computes optimal transfer strategy for each field.
|
|
686
|
+
*/
|
|
687
|
+
private pushFieldsToClient(
|
|
688
|
+
clientId: string,
|
|
689
|
+
entity: string,
|
|
690
|
+
id: string,
|
|
691
|
+
key: EntityKey,
|
|
692
|
+
changedFields: string[],
|
|
693
|
+
newState: Record<string, unknown>,
|
|
694
|
+
): void {
|
|
695
|
+
const client = this.clients.get(clientId);
|
|
696
|
+
if (!client) return;
|
|
697
|
+
|
|
698
|
+
const clientStateMap = this.clientStates.get(clientId);
|
|
699
|
+
if (!clientStateMap) return;
|
|
700
|
+
|
|
701
|
+
const clientEntityState = clientStateMap.get(key);
|
|
702
|
+
if (!clientEntityState) return;
|
|
703
|
+
|
|
704
|
+
const { lastState, fields } = clientEntityState;
|
|
705
|
+
|
|
706
|
+
// Compute updates for changed fields
|
|
707
|
+
const updates: Record<string, Update> = {};
|
|
708
|
+
let hasChanges = false;
|
|
709
|
+
|
|
710
|
+
for (const field of changedFields) {
|
|
711
|
+
// Check if client is subscribed to this field
|
|
712
|
+
if (fields !== "*" && !fields.has(field)) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const oldValue = lastState[field];
|
|
717
|
+
const newValue = newState[field];
|
|
718
|
+
|
|
719
|
+
// Skip if unchanged
|
|
720
|
+
if (oldValue === newValue) continue;
|
|
721
|
+
if (
|
|
722
|
+
typeof oldValue === "object" &&
|
|
723
|
+
typeof newValue === "object" &&
|
|
724
|
+
JSON.stringify(oldValue) === JSON.stringify(newValue)
|
|
725
|
+
) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Compute optimal update for transfer
|
|
730
|
+
const update = createUpdate(oldValue, newValue);
|
|
731
|
+
updates[field] = update;
|
|
732
|
+
hasChanges = true;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!hasChanges) return;
|
|
736
|
+
|
|
737
|
+
// Send update
|
|
738
|
+
client.send({
|
|
739
|
+
type: "update",
|
|
740
|
+
entity,
|
|
741
|
+
id,
|
|
742
|
+
updates,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Update client's last known state
|
|
746
|
+
for (const field of changedFields) {
|
|
747
|
+
if (newState[field] !== undefined) {
|
|
748
|
+
clientEntityState.lastState[field] = newState[field];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
333
753
|
/**
|
|
334
754
|
* Send initial data to a newly subscribed client
|
|
335
755
|
*/
|
|
@@ -426,7 +846,9 @@ export class GraphStateManager {
|
|
|
426
846
|
clear(): void {
|
|
427
847
|
this.clients.clear();
|
|
428
848
|
this.canonical.clear();
|
|
849
|
+
this.canonicalArrays.clear();
|
|
429
850
|
this.clientStates.clear();
|
|
851
|
+
this.clientArrayStates.clear();
|
|
430
852
|
this.entitySubscribers.clear();
|
|
431
853
|
}
|
|
432
854
|
}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAEN,YAAY,EAEZ,KAAK,UAAU,EACf,KAAK,gBAAgB,IAAI,YAAY,EACrC,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,eAAe,EAEpB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAElB,KAAK,aAAa,EAClB,KAAK,UAAU,EAEf,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,GAChB,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAEN,iBAAiB,EAEjB,uBAAuB,EAEvB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,uBAAuB,GAC5B,MAAM,SAAS,CAAC;AAMjB,OAAO,EAEN,UAAU,EAEV,gBAAgB,EAEhB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GAClB,MAAM,eAAe,CAAC"}
|
package/dist/server/create.d.ts
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @sylphx/lens-server - Lens Server
|
|
3
|
-
*
|
|
4
|
-
* Core server implementation:
|
|
5
|
-
* - Free Operations (query/mutation definitions)
|
|
6
|
-
* - GraphStateManager (per-client state tracking, minimal diffs)
|
|
7
|
-
* - Field-level subscriptions
|
|
8
|
-
* - Entity Resolvers with DataLoader batching
|
|
9
|
-
*/
|
|
10
|
-
import { type ContextValue, type EntityDef, type EntityDefinition, type EntityResolvers, type EntityResolversDefinition, type MutationDef, type QueryDef, type RelationDef, type RelationTypeWithForeignKey, type RouterDef } from "@sylphx/lens-core";
|
|
11
|
-
/** Selection object type for nested field selection */
|
|
12
|
-
export interface SelectionObject {
|
|
13
|
-
[key: string]: boolean | SelectionObject | {
|
|
14
|
-
select: SelectionObject;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
import { GraphStateManager } from "../state/graph-state-manager";
|
|
18
|
-
/** Entity map type */
|
|
19
|
-
export type EntitiesMap = Record<string, EntityDef<string, any>>;
|
|
20
|
-
/** Queries map type */
|
|
21
|
-
export type QueriesMap = Record<string, QueryDef<unknown, unknown>>;
|
|
22
|
-
/** Mutations map type */
|
|
23
|
-
export type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
|
|
24
|
-
/** Relations array type */
|
|
25
|
-
export type RelationsArray = RelationDef<EntityDef<string, EntityDefinition>, Record<string, RelationTypeWithForeignKey>>[];
|
|
26
|
-
/** Operation metadata for handshake */
|
|
27
|
-
export interface OperationMeta {
|
|
28
|
-
type: "query" | "mutation";
|
|
29
|
-
optimistic?: unknown;
|
|
30
|
-
}
|
|
31
|
-
/** Nested operations structure for handshake */
|
|
32
|
-
export type OperationsMap = {
|
|
33
|
-
[key: string]: OperationMeta | OperationsMap;
|
|
34
|
-
};
|
|
35
|
-
/** Server configuration */
|
|
36
|
-
export interface LensServerConfig<TContext extends ContextValue = ContextValue> {
|
|
37
|
-
/** Entity definitions */
|
|
38
|
-
entities?: EntitiesMap;
|
|
39
|
-
/** Relation definitions */
|
|
40
|
-
relations?: RelationsArray;
|
|
41
|
-
/** Router definition (namespaced operations) */
|
|
42
|
-
router?: RouterDef;
|
|
43
|
-
/** Query definitions (flat, legacy) */
|
|
44
|
-
queries?: QueriesMap;
|
|
45
|
-
/** Mutation definitions (flat, legacy) */
|
|
46
|
-
mutations?: MutationsMap;
|
|
47
|
-
/** Entity resolvers */
|
|
48
|
-
resolvers?: EntityResolvers<EntityResolversDefinition>;
|
|
49
|
-
/** Context factory */
|
|
50
|
-
context?: (req?: unknown) => TContext | Promise<TContext>;
|
|
51
|
-
/** Server version */
|
|
52
|
-
version?: string;
|
|
53
|
-
}
|
|
54
|
-
/** Server metadata for transport handshake */
|
|
55
|
-
export interface ServerMetadata {
|
|
56
|
-
/** Server version */
|
|
57
|
-
version: string;
|
|
58
|
-
/** Operations metadata map */
|
|
59
|
-
operations: OperationsMap;
|
|
60
|
-
}
|
|
61
|
-
/** Operation for in-process transport */
|
|
62
|
-
export interface LensOperation {
|
|
63
|
-
/** Operation path (e.g., 'user.get', 'session.create') */
|
|
64
|
-
path: string;
|
|
65
|
-
/** Operation input */
|
|
66
|
-
input?: unknown;
|
|
67
|
-
}
|
|
68
|
-
/** Result from operation execution */
|
|
69
|
-
export interface LensResult<T = unknown> {
|
|
70
|
-
/** Success data */
|
|
71
|
-
data?: T;
|
|
72
|
-
/** Error if operation failed */
|
|
73
|
-
error?: Error;
|
|
74
|
-
}
|
|
75
|
-
/** Lens server interface */
|
|
76
|
-
export interface LensServer {
|
|
77
|
-
/** Get server metadata for transport handshake */
|
|
78
|
-
getMetadata(): ServerMetadata;
|
|
79
|
-
/** Execute operation - auto-detects query vs mutation from registered operations */
|
|
80
|
-
execute(op: LensOperation): Promise<LensResult>;
|
|
81
|
-
/** Execute a query (one-time) */
|
|
82
|
-
executeQuery<TInput, TOutput>(name: string, input?: TInput): Promise<TOutput>;
|
|
83
|
-
/** Execute a mutation */
|
|
84
|
-
executeMutation<TInput, TOutput>(name: string, input: TInput): Promise<TOutput>;
|
|
85
|
-
/** Handle WebSocket connection */
|
|
86
|
-
handleWebSocket(ws: WebSocketLike): void;
|
|
87
|
-
/** Handle HTTP request */
|
|
88
|
-
handleRequest(req: Request): Promise<Response>;
|
|
89
|
-
/** Get GraphStateManager for external access */
|
|
90
|
-
getStateManager(): GraphStateManager;
|
|
91
|
-
/** Start server */
|
|
92
|
-
listen(port: number): Promise<void>;
|
|
93
|
-
/** Close server */
|
|
94
|
-
close(): Promise<void>;
|
|
95
|
-
}
|
|
96
|
-
/** WebSocket interface */
|
|
97
|
-
export interface WebSocketLike {
|
|
98
|
-
send(data: string): void;
|
|
99
|
-
close(): void;
|
|
100
|
-
onmessage?: ((event: {
|
|
101
|
-
data: string;
|
|
102
|
-
}) => void) | null;
|
|
103
|
-
onclose?: (() => void) | null;
|
|
104
|
-
onerror?: ((error: unknown) => void) | null;
|
|
105
|
-
}
|
|
106
|
-
declare class LensServerImpl<Q extends QueriesMap = QueriesMap, M extends MutationsMap = MutationsMap, TContext extends ContextValue = ContextValue> implements LensServer {
|
|
107
|
-
private queries;
|
|
108
|
-
private mutations;
|
|
109
|
-
private entities;
|
|
110
|
-
private resolvers?;
|
|
111
|
-
private contextFactory;
|
|
112
|
-
private version;
|
|
113
|
-
private ctx;
|
|
114
|
-
/** GraphStateManager for per-client state tracking */
|
|
115
|
-
private stateManager;
|
|
116
|
-
/** DataLoaders for N+1 batching (per-request) */
|
|
117
|
-
private loaders;
|
|
118
|
-
/** Client connections */
|
|
119
|
-
private connections;
|
|
120
|
-
private connectionCounter;
|
|
121
|
-
/** Server instance */
|
|
122
|
-
private server;
|
|
123
|
-
constructor(config: LensServerConfig<TContext> & {
|
|
124
|
-
queries?: Q;
|
|
125
|
-
mutations?: M;
|
|
126
|
-
});
|
|
127
|
-
getStateManager(): GraphStateManager;
|
|
128
|
-
/**
|
|
129
|
-
* Get server metadata for transport handshake.
|
|
130
|
-
* Used by inProcess transport for direct access.
|
|
131
|
-
*/
|
|
132
|
-
getMetadata(): ServerMetadata;
|
|
133
|
-
/**
|
|
134
|
-
* Execute operation - auto-detects query vs mutation from registered operations.
|
|
135
|
-
* Used by inProcess transport for direct server calls.
|
|
136
|
-
*/
|
|
137
|
-
execute(op: LensOperation): Promise<LensResult>;
|
|
138
|
-
/**
|
|
139
|
-
* Build nested operations map for handshake response
|
|
140
|
-
* Converts flat "user.get", "user.create" into nested { user: { get: {...}, create: {...} } }
|
|
141
|
-
*/
|
|
142
|
-
private buildOperationsMap;
|
|
143
|
-
handleWebSocket(ws: WebSocketLike): void;
|
|
144
|
-
private handleMessage;
|
|
145
|
-
private handleHandshake;
|
|
146
|
-
private handleSubscribe;
|
|
147
|
-
private executeSubscription;
|
|
148
|
-
private handleUpdateFields;
|
|
149
|
-
private handleUnsubscribe;
|
|
150
|
-
private handleQuery;
|
|
151
|
-
private handleMutation;
|
|
152
|
-
private handleDisconnect;
|
|
153
|
-
executeQuery<TInput, TOutput>(name: string, input?: TInput): Promise<TOutput>;
|
|
154
|
-
executeMutation<TInput, TOutput>(name: string, input: TInput): Promise<TOutput>;
|
|
155
|
-
handleRequest(req: Request): Promise<Response>;
|
|
156
|
-
listen(port: number): Promise<void>;
|
|
157
|
-
close(): Promise<void>;
|
|
158
|
-
private findConnectionByWs;
|
|
159
|
-
private getEntityNameFromOutput;
|
|
160
|
-
private getEntityNameFromMutation;
|
|
161
|
-
private extractEntities;
|
|
162
|
-
private applySelection;
|
|
163
|
-
private applySelectionToObject;
|
|
164
|
-
/**
|
|
165
|
-
* Execute entity resolvers for nested data.
|
|
166
|
-
* Processes the selection object and resolves relation fields.
|
|
167
|
-
*/
|
|
168
|
-
private executeEntityResolvers;
|
|
169
|
-
/**
|
|
170
|
-
* Get target entity name for a relation field.
|
|
171
|
-
*/
|
|
172
|
-
private getRelationTargetEntity;
|
|
173
|
-
/**
|
|
174
|
-
* Serialize entity data for transport.
|
|
175
|
-
* Auto-calls serialize() on field types (Date → ISO string, etc.)
|
|
176
|
-
*/
|
|
177
|
-
private serializeEntity;
|
|
178
|
-
/**
|
|
179
|
-
* Process query result: execute entity resolvers, apply selection, serialize
|
|
180
|
-
*/
|
|
181
|
-
private processQueryResult;
|
|
182
|
-
private computeUpdates;
|
|
183
|
-
private deepEqual;
|
|
184
|
-
private clearLoaders;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Infer input type from a query/mutation definition
|
|
188
|
-
*/
|
|
189
|
-
export type InferInput<T> = T extends QueryDef<infer I, unknown> ? I extends void ? void : I : T extends MutationDef<infer I, unknown> ? I : never;
|
|
190
|
-
/**
|
|
191
|
-
* Infer output type from a query/mutation definition
|
|
192
|
-
*/
|
|
193
|
-
export type InferOutput<T> = T extends QueryDef<unknown, infer O> ? O : T extends MutationDef<unknown, infer O> ? O : never;
|
|
194
|
-
/**
|
|
195
|
-
* API type for client inference
|
|
196
|
-
* Export this type for client-side type safety
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* ```typescript
|
|
200
|
-
* // Server
|
|
201
|
-
* const server = createLensServer({ queries, mutations });
|
|
202
|
-
* export type Api = InferApi<typeof server>;
|
|
203
|
-
*
|
|
204
|
-
* // Client (only imports TYPE)
|
|
205
|
-
* import type { Api } from './server';
|
|
206
|
-
* const client = createClient<Api>({ links: [...] });
|
|
207
|
-
* ```
|
|
208
|
-
*/
|
|
209
|
-
export type InferApi<T extends LensServer> = T extends LensServerImpl<infer Q, infer M> ? {
|
|
210
|
-
queries: Q;
|
|
211
|
-
mutations: M;
|
|
212
|
-
} : never;
|
|
213
|
-
/**
|
|
214
|
-
* Create Lens server with Operations API + Optimization Layer
|
|
215
|
-
*/
|
|
216
|
-
export declare function createServer<TContext extends ContextValue = ContextValue, Q extends QueriesMap = QueriesMap, M extends MutationsMap = MutationsMap>(config: LensServerConfig<TContext> & {
|
|
217
|
-
queries?: Q;
|
|
218
|
-
mutations?: M;
|
|
219
|
-
}): LensServer & {
|
|
220
|
-
_types: {
|
|
221
|
-
queries: Q;
|
|
222
|
-
mutations: M;
|
|
223
|
-
};
|
|
224
|
-
};
|
|
225
|
-
export {};
|
|
226
|
-
//# sourceMappingURL=create.d.ts.map
|