@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.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 (isEntityDef(model) || isModelDef(model)) {
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 (!isEntityDef(entity) && !isModelDef(entity))
407
+ if (!isModelDef(entity))
394
408
  continue;
395
409
  if (resolverMap.has(name))
396
410
  continue;
397
- if (hasInlineResolvers(entity)) {
398
- const resolver = createResolverFromEntity(entity);
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
- if (!isQuery && !isMutation) {
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
- const result = def._input.safeParse(cleanInput);
487
- if (!result.success) {
488
- observer.next?.({
489
- $: "error",
490
- error: `Invalid input: ${JSON.stringify(result.error)}`,
491
- code: "VALIDATION_ERROR"
492
- });
493
- observer.complete?.();
494
- return;
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, input: cleanInput, ctx: lensContext });
655
+ const result = resolver({ args: cleanInput, ctx: lensContext });
531
656
  if (isAsyncIterable(result)) {
532
- for await (const value of result) {
533
- if (cancelled)
534
- break;
535
- const processed = await this.processQueryResult(path, value, select, context, onCleanup, createFieldEmit);
536
- emitIfChanged(processed);
537
- }
538
- if (!cancelled) {
539
- observer.complete?.();
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
- if (!isQuery && !cancelled) {
570
- observer.complete?.();
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 processed = await this.resolveEntityFields(data, nestedInputs, context, "", onCleanup, createFieldEmit, new Set);
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
- } catch {
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 (!isEntityDef(def))
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
- return bestMatch?.name;
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 = this.loaders.get(loaderKey);
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
- this.loaders.set(loaderKey, loader);
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 isSubscription = def._resolve?.constructor?.name === "AsyncGeneratorFunction" || def._resolve?.constructor?.name === "GeneratorFunction";
832
- const opType = isSubscription ? "subscription" : "query";
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
- const operationPath2 = body.operation ?? body.path;
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: operationPath2,
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
- const input = inputParam ? JSON.parse(inputParam) : undefined;
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
- const result = server.execute({ path, input });
1281
- if (result && typeof result === "object" && "subscribe" in result) {
1282
- const observable = result;
1283
- const subscription = observable.subscribe({
1284
- next: (value) => {
1285
- const data = `data: ${JSON.stringify(value.data)}
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
- controller.enqueue(encoder.encode(data));
1289
- },
1290
- error: (err) => {
1291
- const data = `event: error
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
- controller.enqueue(encoder.encode(data));
1296
- controller.close();
1297
- },
1298
- complete: () => {
1299
- controller.close();
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 [entity, entityId] = entityKey.split(":");
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 (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
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 JSON.stringify(state);
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: { id: $input("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 = this.entityIndex.get(removed.entityKey);
2503
- if (indices && indices.length > 0) {
2504
- indices.shift();
2505
- if (indices.length === 0) {
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[indices[0] - 1];
2714
+ const nextEntry = this.entries[removedEntityIndices[0]];
2511
2715
  if (nextEntry) {
2512
2716
  this.oldestVersionIndex.set(removed.entityKey, nextEntry.version);
2513
2717
  }