@sylphx/lens-server 2.2.0 → 2.3.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.js CHANGED
@@ -40,7 +40,8 @@ import {
40
40
  isEntityDef,
41
41
  isMutationDef,
42
42
  isQueryDef,
43
- toResolverMap
43
+ toResolverMap,
44
+ valuesEqual
44
45
  } from "@sylphx/lens-core";
45
46
 
46
47
  // src/plugin/types.ts
@@ -215,6 +216,9 @@ class DataLoader {
215
216
  }
216
217
 
217
218
  // src/server/selection.ts
219
+ function isNestedSelection(value) {
220
+ return typeof value === "object" && value !== null && (("input" in value) || ("select" in value)) && !Array.isArray(value);
221
+ }
218
222
  function extractSelect(value) {
219
223
  if (value === true)
220
224
  return null;
@@ -257,6 +261,25 @@ function applySelection(data, select) {
257
261
  }
258
262
  return result;
259
263
  }
264
+ function extractNestedInputs(select, prefix = "") {
265
+ const inputs = new Map;
266
+ for (const [key, value] of Object.entries(select)) {
267
+ const path = prefix ? `${prefix}.${key}` : key;
268
+ if (isNestedSelection(value) && value.input) {
269
+ inputs.set(path, value.input);
270
+ }
271
+ if (typeof value === "object" && value !== null) {
272
+ const nestedSelect = extractSelect(value);
273
+ if (nestedSelect) {
274
+ const nestedInputs = extractNestedInputs(nestedSelect, path);
275
+ for (const [nestedPath, nestedInput] of nestedInputs) {
276
+ inputs.set(nestedPath, nestedInput);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ return inputs;
282
+ }
260
283
 
261
284
  // src/server/create.ts
262
285
  function isAsyncIterable(value) {
@@ -357,7 +380,16 @@ class LensServerImpl {
357
380
  subscribe: (observer) => {
358
381
  let cancelled = false;
359
382
  let currentState;
383
+ let lastEmittedResult;
360
384
  const cleanups = [];
385
+ const emitIfChanged = (data) => {
386
+ if (cancelled)
387
+ return;
388
+ if (valuesEqual(data, lastEmittedResult))
389
+ return;
390
+ lastEmittedResult = data;
391
+ observer.next?.({ data });
392
+ };
361
393
  (async () => {
362
394
  try {
363
395
  const def = isQuery ? this.queries[path] : this.mutations[path];
@@ -391,11 +423,32 @@ class LensServerImpl {
391
423
  observer.complete?.();
392
424
  return;
393
425
  }
426
+ let emitProcessing = false;
427
+ const emitQueue = [];
428
+ const processEmitQueue = async () => {
429
+ if (emitProcessing || cancelled)
430
+ return;
431
+ emitProcessing = true;
432
+ while (emitQueue.length > 0 && !cancelled) {
433
+ const command = emitQueue.shift();
434
+ currentState = this.applyEmitCommand(command, currentState);
435
+ const fieldEmitFactory = isQuery ? this.createFieldEmitFactory(() => currentState, (state) => {
436
+ currentState = state;
437
+ }, emitIfChanged, select, context, onCleanup) : undefined;
438
+ const processed = isQuery ? await this.processQueryResult(path, currentState, select, context, onCleanup, fieldEmitFactory) : currentState;
439
+ emitIfChanged(processed);
440
+ }
441
+ emitProcessing = false;
442
+ };
394
443
  const emitHandler = (command) => {
395
444
  if (cancelled)
396
445
  return;
397
- currentState = this.applyEmitCommand(command, currentState);
398
- observer.next?.({ data: currentState });
446
+ emitQueue.push(command);
447
+ processEmitQueue().catch((err) => {
448
+ if (!cancelled) {
449
+ observer.next?.({ error: err instanceof Error ? err : new Error(String(err)) });
450
+ }
451
+ });
399
452
  };
400
453
  const emit = createEmit(emitHandler);
401
454
  const onCleanup = (fn) => {
@@ -406,6 +459,9 @@ class LensServerImpl {
406
459
  cleanups.splice(idx, 1);
407
460
  };
408
461
  };
462
+ const createFieldEmit = isQuery ? this.createFieldEmitFactory(() => currentState, (state) => {
463
+ currentState = state;
464
+ }, emitIfChanged, select, context, onCleanup) : undefined;
409
465
  const lensContext = { ...context, emit, onCleanup };
410
466
  const result = resolver({ input: cleanInput, ctx: lensContext });
411
467
  if (isAsyncIterable(result)) {
@@ -413,8 +469,8 @@ class LensServerImpl {
413
469
  if (cancelled)
414
470
  break;
415
471
  currentState = value;
416
- const processed = await this.processQueryResult(path, value, select);
417
- observer.next?.({ data: processed });
472
+ const processed = await this.processQueryResult(path, value, select, context, onCleanup, createFieldEmit);
473
+ emitIfChanged(processed);
418
474
  }
419
475
  if (!cancelled) {
420
476
  observer.complete?.();
@@ -422,10 +478,8 @@ class LensServerImpl {
422
478
  } else {
423
479
  const value = await result;
424
480
  currentState = value;
425
- const processed = isQuery ? await this.processQueryResult(path, value, select) : value;
426
- if (!cancelled) {
427
- observer.next?.({ data: processed });
428
- }
481
+ const processed = isQuery ? await this.processQueryResult(path, value, select, context, onCleanup, createFieldEmit) : value;
482
+ emitIfChanged(processed);
429
483
  }
430
484
  });
431
485
  } catch (error) {
@@ -500,20 +554,59 @@ class LensServerImpl {
500
554
  return state;
501
555
  }
502
556
  }
503
- async processQueryResult(_operationName, data, select) {
557
+ createFieldEmitFactory(getCurrentState, setCurrentState, notifyObserver, select, context, onCleanup) {
558
+ return (fieldPath) => {
559
+ if (!fieldPath)
560
+ return;
561
+ return (newValue) => {
562
+ const state = getCurrentState();
563
+ if (!state || typeof state !== "object")
564
+ return;
565
+ const updatedState = this.setFieldByPath(state, fieldPath, newValue);
566
+ setCurrentState(updatedState);
567
+ (async () => {
568
+ try {
569
+ const nestedInputs = select ? extractNestedInputs(select) : undefined;
570
+ const processed = await this.resolveEntityFields(updatedState, nestedInputs, context, "", onCleanup, this.createFieldEmitFactory(getCurrentState, setCurrentState, notifyObserver, select, context, onCleanup));
571
+ const result = select ? applySelection(processed, select) : processed;
572
+ notifyObserver(result);
573
+ } catch (err) {
574
+ console.error(`Field emit error at path "${fieldPath}":`, err);
575
+ }
576
+ })();
577
+ };
578
+ };
579
+ }
580
+ setFieldByPath(obj, path, value) {
581
+ const parts = path.split(".");
582
+ if (parts.length === 1) {
583
+ return { ...obj, [path]: value };
584
+ }
585
+ const [first, ...rest] = parts;
586
+ const nested = obj[first];
587
+ if (nested && typeof nested === "object") {
588
+ return {
589
+ ...obj,
590
+ [first]: this.setFieldByPath(nested, rest.join("."), value)
591
+ };
592
+ }
593
+ return obj;
594
+ }
595
+ async processQueryResult(_operationName, data, select, context, onCleanup, createFieldEmit) {
504
596
  if (!data)
505
597
  return data;
506
- const processed = await this.resolveEntityFields(data);
598
+ const nestedInputs = select ? extractNestedInputs(select) : undefined;
599
+ const processed = await this.resolveEntityFields(data, nestedInputs, context, "", onCleanup, createFieldEmit);
507
600
  if (select) {
508
601
  return applySelection(processed, select);
509
602
  }
510
603
  return processed;
511
604
  }
512
- async resolveEntityFields(data) {
605
+ async resolveEntityFields(data, nestedInputs, context, fieldPath = "", onCleanup, createFieldEmit) {
513
606
  if (!data || !this.resolverMap)
514
607
  return data;
515
608
  if (Array.isArray(data)) {
516
- return Promise.all(data.map((item) => this.resolveEntityFields(item)));
609
+ return Promise.all(data.map((item) => this.resolveEntityFields(item, nestedInputs, context, fieldPath, onCleanup, createFieldEmit)));
517
610
  }
518
611
  if (typeof data !== "object")
519
612
  return data;
@@ -529,15 +622,31 @@ class LensServerImpl {
529
622
  const field = String(fieldName);
530
623
  if (resolverDef.isExposed(field))
531
624
  continue;
625
+ const currentPath = fieldPath ? `${fieldPath}.${field}` : field;
626
+ const args = nestedInputs?.get(currentPath) ?? {};
627
+ const hasArgs = Object.keys(args).length > 0;
532
628
  const existingValue = result[field];
533
629
  if (existingValue !== undefined) {
534
- result[field] = await this.resolveEntityFields(existingValue);
630
+ result[field] = await this.resolveEntityFields(existingValue, nestedInputs, context, currentPath, onCleanup, createFieldEmit);
535
631
  continue;
536
632
  }
537
- const loaderKey = `${typeName}.${field}`;
538
- const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field);
539
- result[field] = await loader.load(obj);
540
- result[field] = await this.resolveEntityFields(result[field]);
633
+ if (hasArgs || context) {
634
+ try {
635
+ const extendedCtx = {
636
+ ...context ?? {},
637
+ emit: createFieldEmit(currentPath),
638
+ onCleanup
639
+ };
640
+ result[field] = await resolverDef.resolveField(field, obj, args, extendedCtx);
641
+ } catch {
642
+ result[field] = null;
643
+ }
644
+ } else {
645
+ const loaderKey = `${typeName}.${field}`;
646
+ const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field);
647
+ result[field] = await loader.load(obj);
648
+ }
649
+ result[field] = await this.resolveEntityFields(result[field], nestedInputs, context, currentPath, onCleanup, createFieldEmit);
541
650
  }
542
651
  return result;
543
652
  }
@@ -546,15 +655,23 @@ class LensServerImpl {
546
655
  return obj.__typename;
547
656
  if ("_type" in obj)
548
657
  return obj._type;
658
+ let bestMatch;
549
659
  for (const [name, def] of Object.entries(this.entities)) {
550
- if (isEntityDef(def) && this.matchesEntity(obj, def)) {
551
- return name;
660
+ if (!isEntityDef(def))
661
+ continue;
662
+ const score = this.getEntityMatchScore(obj, def);
663
+ if (score >= 0.5 && (!bestMatch || score > bestMatch.score)) {
664
+ bestMatch = { name, score };
552
665
  }
553
666
  }
554
- return;
667
+ return bestMatch?.name;
555
668
  }
556
- matchesEntity(obj, entityDef) {
557
- return "id" in obj || entityDef._name in obj;
669
+ getEntityMatchScore(obj, entityDef) {
670
+ const fieldNames = Object.keys(entityDef.fields);
671
+ if (fieldNames.length === 0)
672
+ return 0;
673
+ const matchingFields = fieldNames.filter((field) => (field in obj));
674
+ return matchingFields.length / fieldNames.length;
558
675
  }
559
676
  getOrCreateLoaderForField(loaderKey, resolverDef, fieldName) {
560
677
  let loader = this.loaders.get(loaderKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-server",
3
- "version": "2.2.0",
3
+ "version": "2.3.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": "^2.1.0"
33
+ "@sylphx/lens-core": "^2.2.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.9.3",