@sylphx/lens-server 3.0.1 → 4.0.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 +16 -7
- package/dist/index.js +318 -114
- package/package.json +2 -2
- package/src/e2e/server.test.ts +70 -56
- package/src/handlers/framework.ts +65 -32
- package/src/handlers/http.test.ts +8 -8
- package/src/handlers/http.ts +3 -5
- package/src/handlers/ws-types.ts +1 -0
- package/src/handlers/ws.test.ts +6 -6
- package/src/handlers/ws.ts +14 -3
- package/src/index.ts +0 -2
- package/src/plugin/optimistic.ts +6 -6
- package/src/reconnect/operation-log.ts +20 -9
- package/src/server/create.test.ts +223 -316
- package/src/server/create.ts +328 -123
- package/src/server/types.ts +23 -6
- package/src/storage/memory.ts +24 -3
package/dist/index.js
CHANGED
|
@@ -41,12 +41,11 @@ import {
|
|
|
41
41
|
createResolverFromEntity,
|
|
42
42
|
flattenRouter,
|
|
43
43
|
hashValue,
|
|
44
|
-
hasInlineResolvers,
|
|
45
|
-
isEntityDef,
|
|
46
44
|
isLiveQueryDef,
|
|
47
45
|
isModelDef,
|
|
48
46
|
isMutationDef,
|
|
49
47
|
isQueryDef,
|
|
48
|
+
isSubscriptionDef,
|
|
50
49
|
mergeModelCollections,
|
|
51
50
|
toOps,
|
|
52
51
|
valuesEqual
|
|
@@ -298,17 +297,19 @@ var noopLogger = {};
|
|
|
298
297
|
class LensServerImpl {
|
|
299
298
|
queries;
|
|
300
299
|
mutations;
|
|
300
|
+
subscriptions;
|
|
301
301
|
entities;
|
|
302
302
|
resolverMap;
|
|
303
303
|
contextFactory;
|
|
304
304
|
version;
|
|
305
305
|
logger;
|
|
306
306
|
ctx = createContext();
|
|
307
|
-
loaders = new Map;
|
|
308
307
|
pluginManager;
|
|
308
|
+
typeNameCache = new WeakMap;
|
|
309
309
|
constructor(config) {
|
|
310
310
|
const queries = { ...config.queries ?? {} };
|
|
311
311
|
const mutations = { ...config.mutations ?? {} };
|
|
312
|
+
const subscriptions = { ...config.subscriptions ?? {} };
|
|
312
313
|
if (config.router) {
|
|
313
314
|
const flattened = flattenRouter(config.router);
|
|
314
315
|
for (const [path, procedure] of flattened) {
|
|
@@ -316,11 +317,14 @@ class LensServerImpl {
|
|
|
316
317
|
queries[path] = procedure;
|
|
317
318
|
} else if (isMutationDef(procedure)) {
|
|
318
319
|
mutations[path] = procedure;
|
|
320
|
+
} else if (isSubscriptionDef(procedure)) {
|
|
321
|
+
subscriptions[path] = procedure;
|
|
319
322
|
}
|
|
320
323
|
}
|
|
321
324
|
}
|
|
322
325
|
this.queries = queries;
|
|
323
326
|
this.mutations = mutations;
|
|
327
|
+
this.subscriptions = subscriptions;
|
|
324
328
|
const autoCollected = config.router ? collectModelsFromRouter(config.router) : collectModelsFromOperations(queries, mutations);
|
|
325
329
|
const entitiesFromConfig = config.entities ?? {};
|
|
326
330
|
const mergedModels = mergeModelCollections(autoCollected, entitiesFromConfig);
|
|
@@ -334,7 +338,7 @@ class LensServerImpl {
|
|
|
334
338
|
}
|
|
335
339
|
const entities = {};
|
|
336
340
|
for (const [name, model] of mergedModels) {
|
|
337
|
-
if (
|
|
341
|
+
if (isModelDef(model)) {
|
|
338
342
|
entities[name] = model;
|
|
339
343
|
}
|
|
340
344
|
}
|
|
@@ -366,6 +370,11 @@ class LensServerImpl {
|
|
|
366
370
|
def._name = name;
|
|
367
371
|
}
|
|
368
372
|
}
|
|
373
|
+
for (const [name, def] of Object.entries(this.subscriptions)) {
|
|
374
|
+
if (def && typeof def === "object") {
|
|
375
|
+
def._name = name;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
369
378
|
}
|
|
370
379
|
validateDefinitions() {
|
|
371
380
|
for (const [name, def] of Object.entries(this.queries)) {
|
|
@@ -378,6 +387,11 @@ class LensServerImpl {
|
|
|
378
387
|
throw new Error(`Invalid mutation definition: ${name}`);
|
|
379
388
|
}
|
|
380
389
|
}
|
|
390
|
+
for (const [name, def] of Object.entries(this.subscriptions)) {
|
|
391
|
+
if (!isSubscriptionDef(def)) {
|
|
392
|
+
throw new Error(`Invalid subscription definition: ${name}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
381
395
|
}
|
|
382
396
|
buildResolverMap(explicitResolvers, entities) {
|
|
383
397
|
const resolverMap = new Map;
|
|
@@ -390,14 +404,12 @@ class LensServerImpl {
|
|
|
390
404
|
}
|
|
391
405
|
}
|
|
392
406
|
for (const [name, entity] of Object.entries(entities)) {
|
|
393
|
-
if (!
|
|
407
|
+
if (!isModelDef(entity))
|
|
394
408
|
continue;
|
|
395
409
|
if (resolverMap.has(name))
|
|
396
410
|
continue;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
resolverMap.set(name, resolver);
|
|
400
|
-
}
|
|
411
|
+
const resolver = createResolverFromEntity(entity);
|
|
412
|
+
resolverMap.set(name, resolver);
|
|
401
413
|
}
|
|
402
414
|
return resolverMap.size > 0 ? resolverMap : undefined;
|
|
403
415
|
}
|
|
@@ -430,7 +442,8 @@ class LensServerImpl {
|
|
|
430
442
|
const { path, input } = op;
|
|
431
443
|
const isQuery = !!this.queries[path];
|
|
432
444
|
const isMutation = !!this.mutations[path];
|
|
433
|
-
|
|
445
|
+
const isSubscription = !!this.subscriptions[path];
|
|
446
|
+
if (!isQuery && !isMutation && !isSubscription) {
|
|
434
447
|
return {
|
|
435
448
|
subscribe: (observer) => {
|
|
436
449
|
observer.next?.({
|
|
@@ -443,8 +456,106 @@ class LensServerImpl {
|
|
|
443
456
|
}
|
|
444
457
|
};
|
|
445
458
|
}
|
|
459
|
+
if (isSubscription) {
|
|
460
|
+
return this.executeSubscription(path, input);
|
|
461
|
+
}
|
|
446
462
|
return this.executeAsObservable(path, input, isQuery);
|
|
447
463
|
}
|
|
464
|
+
executeSubscription(path, input) {
|
|
465
|
+
return {
|
|
466
|
+
subscribe: (observer) => {
|
|
467
|
+
let cancelled = false;
|
|
468
|
+
const cleanups = [];
|
|
469
|
+
(async () => {
|
|
470
|
+
try {
|
|
471
|
+
const def = this.subscriptions[path];
|
|
472
|
+
if (!def) {
|
|
473
|
+
observer.next?.({
|
|
474
|
+
$: "error",
|
|
475
|
+
error: `Subscription not found: ${path}`,
|
|
476
|
+
code: "NOT_FOUND"
|
|
477
|
+
});
|
|
478
|
+
observer.complete?.();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (def._input && input !== undefined) {
|
|
482
|
+
if (def._input.safeParse) {
|
|
483
|
+
const result = def._input.safeParse(input);
|
|
484
|
+
if (!result.success) {
|
|
485
|
+
observer.next?.({
|
|
486
|
+
$: "error",
|
|
487
|
+
error: `Invalid input: ${JSON.stringify(result.error)}`,
|
|
488
|
+
code: "VALIDATION_ERROR"
|
|
489
|
+
});
|
|
490
|
+
observer.complete?.();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
try {
|
|
495
|
+
def._input.parse(input);
|
|
496
|
+
} catch (e) {
|
|
497
|
+
observer.next?.({
|
|
498
|
+
$: "error",
|
|
499
|
+
error: `Invalid input: ${e instanceof Error ? e.message : String(e)}`,
|
|
500
|
+
code: "VALIDATION_ERROR"
|
|
501
|
+
});
|
|
502
|
+
observer.complete?.();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const context = await this.contextFactory();
|
|
508
|
+
const subscriber = def._subscriber;
|
|
509
|
+
if (!subscriber) {
|
|
510
|
+
observer.next?.({
|
|
511
|
+
$: "error",
|
|
512
|
+
error: `Subscription ${path} has no subscriber`,
|
|
513
|
+
code: "NO_SUBSCRIBER"
|
|
514
|
+
});
|
|
515
|
+
observer.complete?.();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const publisher = subscriber({ input, ctx: context });
|
|
519
|
+
if (publisher) {
|
|
520
|
+
const emit = createEmit((command) => {
|
|
521
|
+
if (cancelled)
|
|
522
|
+
return;
|
|
523
|
+
if (command.type === "full") {
|
|
524
|
+
observer.next?.({ $: "snapshot", data: command.data });
|
|
525
|
+
}
|
|
526
|
+
}, "scalar");
|
|
527
|
+
publisher({
|
|
528
|
+
emit,
|
|
529
|
+
onCleanup: (fn) => {
|
|
530
|
+
cleanups.push(fn);
|
|
531
|
+
return fn;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
for (const fn of cleanups) {
|
|
537
|
+
try {
|
|
538
|
+
fn();
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
if (!cancelled) {
|
|
542
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
543
|
+
observer.next?.({ $: "error", error: errMsg, code: "INTERNAL_ERROR" });
|
|
544
|
+
observer.complete?.();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
})();
|
|
548
|
+
return {
|
|
549
|
+
unsubscribe: () => {
|
|
550
|
+
cancelled = true;
|
|
551
|
+
for (const fn of cleanups) {
|
|
552
|
+
fn();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
448
559
|
executeAsObservable(path, input, isQuery) {
|
|
449
560
|
return {
|
|
450
561
|
subscribe: (observer) => {
|
|
@@ -483,15 +594,29 @@ class LensServerImpl {
|
|
|
483
594
|
cleanInput = Object.keys(rest).length > 0 ? rest : undefined;
|
|
484
595
|
}
|
|
485
596
|
if (def._input && cleanInput !== undefined) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
597
|
+
if (def._input.safeParse) {
|
|
598
|
+
const result = def._input.safeParse(cleanInput);
|
|
599
|
+
if (!result.success) {
|
|
600
|
+
observer.next?.({
|
|
601
|
+
$: "error",
|
|
602
|
+
error: `Invalid input: ${JSON.stringify(result.error)}`,
|
|
603
|
+
code: "VALIDATION_ERROR"
|
|
604
|
+
});
|
|
605
|
+
observer.complete?.();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
try {
|
|
610
|
+
def._input.parse(cleanInput);
|
|
611
|
+
} catch (e) {
|
|
612
|
+
observer.next?.({
|
|
613
|
+
$: "error",
|
|
614
|
+
error: `Invalid input: ${e instanceof Error ? e.message : String(e)}`,
|
|
615
|
+
code: "VALIDATION_ERROR"
|
|
616
|
+
});
|
|
617
|
+
observer.complete?.();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
495
620
|
}
|
|
496
621
|
}
|
|
497
622
|
const context = await this.contextFactory();
|
|
@@ -527,16 +652,34 @@ class LensServerImpl {
|
|
|
527
652
|
observer.next?.({ $: "ops", ops });
|
|
528
653
|
}) : undefined;
|
|
529
654
|
const lensContext = { ...context, emit, onCleanup };
|
|
530
|
-
const result = resolver({ args: cleanInput,
|
|
655
|
+
const result = resolver({ args: cleanInput, ctx: lensContext });
|
|
531
656
|
if (isAsyncIterable(result)) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
657
|
+
try {
|
|
658
|
+
for await (const value of result) {
|
|
659
|
+
if (cancelled)
|
|
660
|
+
break;
|
|
661
|
+
const processed = await this.processQueryResult(path, value, select, context, onCleanup, createFieldEmit);
|
|
662
|
+
emitIfChanged(processed);
|
|
663
|
+
}
|
|
664
|
+
if (!cancelled) {
|
|
665
|
+
observer.complete?.();
|
|
666
|
+
}
|
|
667
|
+
} catch (iterError) {
|
|
668
|
+
for (const fn of cleanups) {
|
|
669
|
+
try {
|
|
670
|
+
fn();
|
|
671
|
+
} catch {}
|
|
672
|
+
}
|
|
673
|
+
if (!cancelled) {
|
|
674
|
+
const errMsg = iterError instanceof Error ? iterError.message : String(iterError);
|
|
675
|
+
observer.next?.({
|
|
676
|
+
$: "error",
|
|
677
|
+
error: errMsg,
|
|
678
|
+
code: "STREAM_ERROR"
|
|
679
|
+
});
|
|
680
|
+
observer.complete?.();
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
540
683
|
}
|
|
541
684
|
} else {
|
|
542
685
|
const value = await result;
|
|
@@ -548,7 +691,6 @@ class LensServerImpl {
|
|
|
548
691
|
try {
|
|
549
692
|
const publisher = liveQuery._subscriber({
|
|
550
693
|
args: cleanInput,
|
|
551
|
-
input: cleanInput,
|
|
552
694
|
ctx: context
|
|
553
695
|
});
|
|
554
696
|
if (publisher) {
|
|
@@ -566,19 +708,26 @@ class LensServerImpl {
|
|
|
566
708
|
}
|
|
567
709
|
}
|
|
568
710
|
}
|
|
569
|
-
|
|
570
|
-
|
|
711
|
+
const hasLiveSubscriptions = cleanups.length > 0;
|
|
712
|
+
const isLiveQuery = isQuery && isLiveQueryDef(def) && def._subscriber;
|
|
713
|
+
if (!cancelled) {
|
|
714
|
+
if (!isQuery || !hasLiveSubscriptions && !isLiveQuery) {
|
|
715
|
+
observer.complete?.();
|
|
716
|
+
}
|
|
571
717
|
}
|
|
572
718
|
}
|
|
573
719
|
});
|
|
574
720
|
} catch (error) {
|
|
721
|
+
for (const fn of cleanups) {
|
|
722
|
+
try {
|
|
723
|
+
fn();
|
|
724
|
+
} catch {}
|
|
725
|
+
}
|
|
575
726
|
if (!cancelled) {
|
|
576
727
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
577
728
|
observer.next?.({ $: "error", error: errMsg, code: "INTERNAL_ERROR" });
|
|
578
729
|
observer.complete?.();
|
|
579
730
|
}
|
|
580
|
-
} finally {
|
|
581
|
-
this.clearLoaders();
|
|
582
731
|
}
|
|
583
732
|
})();
|
|
584
733
|
return {
|
|
@@ -645,17 +794,18 @@ class LensServerImpl {
|
|
|
645
794
|
if (!data)
|
|
646
795
|
return data;
|
|
647
796
|
const nestedInputs = select ? extractNestedInputs(select) : undefined;
|
|
648
|
-
const
|
|
797
|
+
const requestLoaders = new Map;
|
|
798
|
+
const processed = await this.resolveEntityFields(data, nestedInputs, context, "", onCleanup, createFieldEmit, new Set, requestLoaders);
|
|
649
799
|
if (select) {
|
|
650
800
|
return applySelection(processed, select);
|
|
651
801
|
}
|
|
652
802
|
return processed;
|
|
653
803
|
}
|
|
654
|
-
async resolveEntityFields(data, nestedInputs, context, fieldPath = "", onCleanup, createFieldEmit, visited = new Set) {
|
|
804
|
+
async resolveEntityFields(data, nestedInputs, context, fieldPath = "", onCleanup, createFieldEmit, visited = new Set, loaders = new Map) {
|
|
655
805
|
if (!data || !this.resolverMap)
|
|
656
806
|
return data;
|
|
657
807
|
if (Array.isArray(data)) {
|
|
658
|
-
return Promise.all(data.map((item) => this.resolveEntityFields(item, nestedInputs, context, fieldPath, onCleanup, createFieldEmit, visited)));
|
|
808
|
+
return Promise.all(data.map((item) => this.resolveEntityFields(item, nestedInputs, context, fieldPath, onCleanup, createFieldEmit, visited, loaders)));
|
|
659
809
|
}
|
|
660
810
|
if (typeof data !== "object")
|
|
661
811
|
return data;
|
|
@@ -682,11 +832,6 @@ class LensServerImpl {
|
|
|
682
832
|
const currentPath = fieldPath ? `${fieldPath}.${field}` : field;
|
|
683
833
|
const args = nestedInputs?.get(currentPath) ?? {};
|
|
684
834
|
const hasArgs = Object.keys(args).length > 0;
|
|
685
|
-
const existingValue = result[field];
|
|
686
|
-
if (existingValue !== undefined) {
|
|
687
|
-
result[field] = await this.resolveEntityFields(existingValue, nestedInputs, context, currentPath, onCleanup, createFieldEmit, visited);
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
835
|
const fieldMode = resolverDef.getFieldMode(field);
|
|
691
836
|
if (fieldMode === "live") {
|
|
692
837
|
try {
|
|
@@ -694,7 +839,7 @@ class LensServerImpl {
|
|
|
694
839
|
result[field] = await resolverDef.resolveField(field, obj, args, context ?? {});
|
|
695
840
|
} else {
|
|
696
841
|
const loaderKey = `${typeName}.${field}`;
|
|
697
|
-
const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field, context ?? {});
|
|
842
|
+
const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field, context ?? {}, loaders);
|
|
698
843
|
result[field] = await loader.load(obj);
|
|
699
844
|
}
|
|
700
845
|
const publisher = resolverDef.subscribeField(field, obj, args, context ?? {});
|
|
@@ -710,31 +855,12 @@ class LensServerImpl {
|
|
|
710
855
|
}
|
|
711
856
|
});
|
|
712
857
|
}
|
|
713
|
-
} catch {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
result[field] = null;
|
|
717
|
-
}
|
|
718
|
-
} else if (fieldMode === "subscribe") {
|
|
719
|
-
try {
|
|
720
|
-
result[field] = null;
|
|
721
|
-
if (createFieldEmit && onCleanup) {
|
|
722
|
-
try {
|
|
723
|
-
const fieldEmit = createFieldEmit(currentPath);
|
|
724
|
-
if (fieldEmit) {
|
|
725
|
-
const legacyCtx = {
|
|
726
|
-
...context ?? {},
|
|
727
|
-
emit: fieldEmit,
|
|
728
|
-
onCleanup: (fn) => {
|
|
729
|
-
onCleanup(fn);
|
|
730
|
-
return fn;
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
resolverDef.subscribeFieldLegacy(field, obj, args, legacyCtx);
|
|
734
|
-
}
|
|
735
|
-
} catch {}
|
|
858
|
+
} catch (subscribeError) {
|
|
859
|
+
this.logger.error?.(`Field subscription error at ${currentPath}:`, subscribeError instanceof Error ? subscribeError.message : String(subscribeError));
|
|
860
|
+
}
|
|
736
861
|
}
|
|
737
|
-
} catch {
|
|
862
|
+
} catch (resolveError) {
|
|
863
|
+
this.logger.error?.(`Live field resolution error at ${currentPath}:`, resolveError instanceof Error ? resolveError.message : String(resolveError));
|
|
738
864
|
result[field] = null;
|
|
739
865
|
}
|
|
740
866
|
} else {
|
|
@@ -743,14 +869,15 @@ class LensServerImpl {
|
|
|
743
869
|
result[field] = await resolverDef.resolveField(field, obj, args, context ?? {});
|
|
744
870
|
} else {
|
|
745
871
|
const loaderKey = `${typeName}.${field}`;
|
|
746
|
-
const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field, context ?? {});
|
|
872
|
+
const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field, context ?? {}, loaders);
|
|
747
873
|
result[field] = await loader.load(obj);
|
|
748
874
|
}
|
|
749
|
-
} catch {
|
|
875
|
+
} catch (resolveError) {
|
|
876
|
+
this.logger.error?.(`Field resolution error at ${currentPath}:`, resolveError instanceof Error ? resolveError.message : String(resolveError));
|
|
750
877
|
result[field] = null;
|
|
751
878
|
}
|
|
752
879
|
}
|
|
753
|
-
result[field] = await this.resolveEntityFields(result[field], nestedInputs, context, currentPath, onCleanup, createFieldEmit, visited);
|
|
880
|
+
result[field] = await this.resolveEntityFields(result[field], nestedInputs, context, currentPath, onCleanup, createFieldEmit, visited, loaders);
|
|
754
881
|
}
|
|
755
882
|
return result;
|
|
756
883
|
}
|
|
@@ -759,16 +886,21 @@ class LensServerImpl {
|
|
|
759
886
|
return obj.__typename;
|
|
760
887
|
if ("_type" in obj)
|
|
761
888
|
return obj._type;
|
|
889
|
+
if (this.typeNameCache.has(obj)) {
|
|
890
|
+
return this.typeNameCache.get(obj);
|
|
891
|
+
}
|
|
762
892
|
let bestMatch;
|
|
763
893
|
for (const [name, def] of Object.entries(this.entities)) {
|
|
764
|
-
if (!
|
|
894
|
+
if (!isModelDef(def))
|
|
765
895
|
continue;
|
|
766
896
|
const score = this.getEntityMatchScore(obj, def);
|
|
767
897
|
if (score >= 0.5 && (!bestMatch || score > bestMatch.score)) {
|
|
768
898
|
bestMatch = { name, score };
|
|
769
899
|
}
|
|
770
900
|
}
|
|
771
|
-
|
|
901
|
+
const result = bestMatch?.name;
|
|
902
|
+
this.typeNameCache.set(obj, result);
|
|
903
|
+
return result;
|
|
772
904
|
}
|
|
773
905
|
getEntityMatchScore(obj, entityDef) {
|
|
774
906
|
const fieldNames = Object.keys(entityDef.fields);
|
|
@@ -777,8 +909,8 @@ class LensServerImpl {
|
|
|
777
909
|
const matchingFields = fieldNames.filter((field) => (field in obj));
|
|
778
910
|
return matchingFields.length / fieldNames.length;
|
|
779
911
|
}
|
|
780
|
-
getOrCreateLoaderForField(loaderKey, resolverDef, fieldName, context) {
|
|
781
|
-
let loader =
|
|
912
|
+
getOrCreateLoaderForField(loaderKey, resolverDef, fieldName, context, loaders) {
|
|
913
|
+
let loader = loaders.get(loaderKey);
|
|
782
914
|
if (!loader) {
|
|
783
915
|
loader = new DataLoader(async (parents) => {
|
|
784
916
|
const results = [];
|
|
@@ -792,13 +924,10 @@ class LensServerImpl {
|
|
|
792
924
|
}
|
|
793
925
|
return results;
|
|
794
926
|
});
|
|
795
|
-
|
|
927
|
+
loaders.set(loaderKey, loader);
|
|
796
928
|
}
|
|
797
929
|
return loader;
|
|
798
930
|
}
|
|
799
|
-
clearLoaders() {
|
|
800
|
-
this.loaders.clear();
|
|
801
|
-
}
|
|
802
931
|
buildOperationsMap() {
|
|
803
932
|
const result = {};
|
|
804
933
|
const setNested = (path, meta) => {
|
|
@@ -828,13 +957,17 @@ class LensServerImpl {
|
|
|
828
957
|
return;
|
|
829
958
|
};
|
|
830
959
|
for (const [name, def] of Object.entries(this.queries)) {
|
|
831
|
-
const
|
|
832
|
-
const
|
|
960
|
+
const isAsyncGenerator = def._resolve?.constructor?.name === "AsyncGeneratorFunction" || def._resolve?.constructor?.name === "GeneratorFunction";
|
|
961
|
+
const isLive = isLiveQueryDef(def);
|
|
962
|
+
const opType = isAsyncGenerator ? "subscription" : "query";
|
|
833
963
|
const returnType = getReturnTypeName(def._output);
|
|
834
964
|
const meta = { type: opType };
|
|
835
965
|
if (returnType) {
|
|
836
966
|
meta.returnType = returnType;
|
|
837
967
|
}
|
|
968
|
+
if (isLive) {
|
|
969
|
+
meta.live = true;
|
|
970
|
+
}
|
|
838
971
|
this.pluginManager.runEnhanceOperationMeta({
|
|
839
972
|
path: name,
|
|
840
973
|
type: opType,
|
|
@@ -857,6 +990,20 @@ class LensServerImpl {
|
|
|
857
990
|
});
|
|
858
991
|
setNested(name, meta);
|
|
859
992
|
}
|
|
993
|
+
for (const [name, def] of Object.entries(this.subscriptions)) {
|
|
994
|
+
const returnType = getReturnTypeName(def._output);
|
|
995
|
+
const meta = { type: "subscription" };
|
|
996
|
+
if (returnType) {
|
|
997
|
+
meta.returnType = returnType;
|
|
998
|
+
}
|
|
999
|
+
this.pluginManager.runEnhanceOperationMeta({
|
|
1000
|
+
path: name,
|
|
1001
|
+
type: "subscription",
|
|
1002
|
+
meta,
|
|
1003
|
+
definition: def
|
|
1004
|
+
});
|
|
1005
|
+
setNested(name, meta);
|
|
1006
|
+
}
|
|
860
1007
|
return result;
|
|
861
1008
|
}
|
|
862
1009
|
getPluginManager() {
|
|
@@ -1122,8 +1269,7 @@ function createHTTPHandler(server, options = {}) {
|
|
|
1122
1269
|
});
|
|
1123
1270
|
}
|
|
1124
1271
|
try {
|
|
1125
|
-
|
|
1126
|
-
if (!operationPath2) {
|
|
1272
|
+
if (!body.path) {
|
|
1127
1273
|
return new Response(JSON.stringify({ error: "Missing operation path" }), {
|
|
1128
1274
|
status: 400,
|
|
1129
1275
|
headers: {
|
|
@@ -1133,7 +1279,7 @@ function createHTTPHandler(server, options = {}) {
|
|
|
1133
1279
|
});
|
|
1134
1280
|
}
|
|
1135
1281
|
const result2 = await firstValueFrom(server.execute({
|
|
1136
|
-
path:
|
|
1282
|
+
path: body.path,
|
|
1137
1283
|
input: body.input
|
|
1138
1284
|
}));
|
|
1139
1285
|
if (isError(result2)) {
|
|
@@ -1273,38 +1419,73 @@ async function handleWebMutation(server, path, request) {
|
|
|
1273
1419
|
}
|
|
1274
1420
|
function handleWebSSE(server, path, url, signal) {
|
|
1275
1421
|
const inputParam = url.searchParams.get("input");
|
|
1276
|
-
|
|
1422
|
+
let input;
|
|
1423
|
+
if (inputParam) {
|
|
1424
|
+
try {
|
|
1425
|
+
input = JSON.parse(inputParam);
|
|
1426
|
+
} catch (parseError) {
|
|
1427
|
+
const encoder = new TextEncoder;
|
|
1428
|
+
const errorStream = new ReadableStream({
|
|
1429
|
+
start(controller) {
|
|
1430
|
+
const errMsg = parseError instanceof Error ? parseError.message : "Invalid JSON";
|
|
1431
|
+
const data = `event: error
|
|
1432
|
+
data: ${JSON.stringify({ error: `Invalid input JSON: ${errMsg}` })}
|
|
1433
|
+
|
|
1434
|
+
`;
|
|
1435
|
+
controller.enqueue(encoder.encode(data));
|
|
1436
|
+
controller.close();
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
return new Response(errorStream, {
|
|
1440
|
+
headers: {
|
|
1441
|
+
"Content-Type": "text/event-stream",
|
|
1442
|
+
"Cache-Control": "no-cache",
|
|
1443
|
+
Connection: "keep-alive"
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1277
1448
|
const stream = new ReadableStream({
|
|
1278
1449
|
start(controller) {
|
|
1279
1450
|
const encoder = new TextEncoder;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1451
|
+
try {
|
|
1452
|
+
const result = server.execute({ path, input });
|
|
1453
|
+
if (result && typeof result === "object" && "subscribe" in result) {
|
|
1454
|
+
const observable = result;
|
|
1455
|
+
const subscription = observable.subscribe({
|
|
1456
|
+
next: (value) => {
|
|
1457
|
+
const data = `data: ${JSON.stringify(value.data)}
|
|
1286
1458
|
|
|
1287
1459
|
`;
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1460
|
+
controller.enqueue(encoder.encode(data));
|
|
1461
|
+
},
|
|
1462
|
+
error: (err) => {
|
|
1463
|
+
const data = `event: error
|
|
1292
1464
|
data: ${JSON.stringify({ error: err.message })}
|
|
1293
1465
|
|
|
1294
1466
|
`;
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
});
|
|
1302
|
-
if (signal) {
|
|
1303
|
-
signal.addEventListener("abort", () => {
|
|
1304
|
-
subscription.unsubscribe();
|
|
1305
|
-
controller.close();
|
|
1467
|
+
controller.enqueue(encoder.encode(data));
|
|
1468
|
+
controller.close();
|
|
1469
|
+
},
|
|
1470
|
+
complete: () => {
|
|
1471
|
+
controller.close();
|
|
1472
|
+
}
|
|
1306
1473
|
});
|
|
1474
|
+
if (signal) {
|
|
1475
|
+
signal.addEventListener("abort", () => {
|
|
1476
|
+
subscription.unsubscribe();
|
|
1477
|
+
controller.close();
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1307
1480
|
}
|
|
1481
|
+
} catch (execError) {
|
|
1482
|
+
const errMsg = execError instanceof Error ? execError.message : "Internal error";
|
|
1483
|
+
const data = `event: error
|
|
1484
|
+
data: ${JSON.stringify({ error: errMsg })}
|
|
1485
|
+
|
|
1486
|
+
`;
|
|
1487
|
+
controller.enqueue(encoder.encode(data));
|
|
1488
|
+
controller.close();
|
|
1308
1489
|
}
|
|
1309
1490
|
}
|
|
1310
1491
|
});
|
|
@@ -1396,7 +1577,9 @@ function createWSHandler(server, options = {}) {
|
|
|
1396
1577
|
...id !== undefined && { id },
|
|
1397
1578
|
error: { code, message }
|
|
1398
1579
|
}));
|
|
1399
|
-
} catch {
|
|
1580
|
+
} catch (sendError) {
|
|
1581
|
+
logger.debug?.(`Failed to send error to client ${conn.id}:`, sendError instanceof Error ? sendError.message : String(sendError));
|
|
1582
|
+
}
|
|
1400
1583
|
}
|
|
1401
1584
|
const connections = new Map;
|
|
1402
1585
|
const wsToConnection = new WeakMap;
|
|
@@ -1643,7 +1826,12 @@ function createWSHandler(server, options = {}) {
|
|
|
1643
1826
|
}
|
|
1644
1827
|
sub.fields = newFields;
|
|
1645
1828
|
for (const entityKey of sub.entityKeys) {
|
|
1646
|
-
const
|
|
1829
|
+
const parts = entityKey.split(":");
|
|
1830
|
+
if (parts.length < 2) {
|
|
1831
|
+
logger.warn?.(`Invalid entityKey format: "${entityKey}" (expected "Entity:id")`);
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const [entity, entityId] = parts;
|
|
1647
1835
|
await pluginManager.runOnUpdateFields({
|
|
1648
1836
|
clientId: conn.id,
|
|
1649
1837
|
subscriptionId: sub.id,
|
|
@@ -1956,6 +2144,17 @@ var DEFAULT_STORAGE_CONFIG = {
|
|
|
1956
2144
|
function makeKey(entity, entityId) {
|
|
1957
2145
|
return `${entity}:${entityId}`;
|
|
1958
2146
|
}
|
|
2147
|
+
function stableStringify(value) {
|
|
2148
|
+
if (value === null || typeof value !== "object") {
|
|
2149
|
+
return JSON.stringify(value);
|
|
2150
|
+
}
|
|
2151
|
+
if (Array.isArray(value)) {
|
|
2152
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
2153
|
+
}
|
|
2154
|
+
const sortedKeys = Object.keys(value).sort();
|
|
2155
|
+
const pairs = sortedKeys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
2156
|
+
return `{${pairs.join(",")}}`;
|
|
2157
|
+
}
|
|
1959
2158
|
function computePatch(oldState, newState) {
|
|
1960
2159
|
const patch = [];
|
|
1961
2160
|
const oldKeys = new Set(Object.keys(oldState));
|
|
@@ -1965,7 +2164,7 @@ function computePatch(oldState, newState) {
|
|
|
1965
2164
|
const newValue = newState[key];
|
|
1966
2165
|
if (!oldKeys.has(key)) {
|
|
1967
2166
|
patch.push({ op: "add", path: `/${key}`, value: newValue });
|
|
1968
|
-
} else if (
|
|
2167
|
+
} else if (stableStringify(oldValue) !== stableStringify(newValue)) {
|
|
1969
2168
|
patch.push({ op: "replace", path: `/${key}`, value: newValue });
|
|
1970
2169
|
}
|
|
1971
2170
|
}
|
|
@@ -1977,7 +2176,7 @@ function computePatch(oldState, newState) {
|
|
|
1977
2176
|
return patch;
|
|
1978
2177
|
}
|
|
1979
2178
|
function hashState(state) {
|
|
1980
|
-
return
|
|
2179
|
+
return stableStringify(state);
|
|
1981
2180
|
}
|
|
1982
2181
|
function memoryStorage(config = {}) {
|
|
1983
2182
|
const cfg = { ...DEFAULT_STORAGE_CONFIG, ...config };
|
|
@@ -2288,7 +2487,7 @@ function sugarToPipeline(sugar, entityType, inputFields) {
|
|
|
2288
2487
|
$do: "entity.delete",
|
|
2289
2488
|
$with: {
|
|
2290
2489
|
type: entity,
|
|
2291
|
-
id:
|
|
2490
|
+
id: $input("id")
|
|
2292
2491
|
},
|
|
2293
2492
|
$as: "result"
|
|
2294
2493
|
}
|
|
@@ -2499,15 +2698,20 @@ class OperationLog {
|
|
|
2499
2698
|
if (!removed)
|
|
2500
2699
|
return;
|
|
2501
2700
|
this.totalMemory -= removed.patchSize;
|
|
2502
|
-
const indices
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2701
|
+
for (const indices of this.entityIndex.values()) {
|
|
2702
|
+
for (let i = 0;i < indices.length; i++) {
|
|
2703
|
+
indices[i]--;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const removedEntityIndices = this.entityIndex.get(removed.entityKey);
|
|
2707
|
+
if (removedEntityIndices && removedEntityIndices.length > 0) {
|
|
2708
|
+
removedEntityIndices.shift();
|
|
2709
|
+
if (removedEntityIndices.length === 0) {
|
|
2506
2710
|
this.entityIndex.delete(removed.entityKey);
|
|
2507
2711
|
this.oldestVersionIndex.delete(removed.entityKey);
|
|
2508
2712
|
this.newestVersionIndex.delete(removed.entityKey);
|
|
2509
2713
|
} else {
|
|
2510
|
-
const nextEntry = this.entries[
|
|
2714
|
+
const nextEntry = this.entries[removedEntityIndices[0]];
|
|
2511
2715
|
if (nextEntry) {
|
|
2512
2716
|
this.oldestVersionIndex.set(removed.entityKey, nextEntry.version);
|
|
2513
2717
|
}
|