@sylphx/lens-server 1.3.2 → 1.5.1

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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @sylphx/lens-server
2
+
3
+ Server runtime for the Lens API framework with WebSocket support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @sylphx/lens-server
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createServer } from "@sylphx/lens-server";
15
+ import { appRouter } from "./router";
16
+
17
+ const server = createServer({ router: appRouter });
18
+
19
+ // Handle WebSocket connections
20
+ Bun.serve({
21
+ port: 3000,
22
+ fetch: server.fetch,
23
+ websocket: server.websocket,
24
+ });
25
+ ```
26
+
27
+ ## License
28
+
29
+ MIT
30
+
31
+ ---
32
+
33
+ Built with [@sylphx/lens-core](https://github.com/SylphxAI/Lens).
34
+
35
+ ✨ Powered by Sylphx
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { query, mutation, router, QueryBuilder, MutationBuilder, QueryDef as QueryDef2, MutationDef as MutationDef2, RouterDef as RouterDef2, RouterRoutes, ResolverFn, ResolverContext, InferRouterContext as InferRouterContext2 } from "@sylphx/lens-core";
2
- import { ContextValue, EntityDef, EntityDefinition, EntityResolvers, EntityResolversDefinition, InferRouterContext, MutationDef, QueryDef, RelationDef, RelationTypeWithForeignKey, RouterDef } from "@sylphx/lens-core";
1
+ import { InferRouterContext as InferRouterContext2, MutationDef as MutationDef2, mutation, QueryDef as QueryDef2, query, ResolverContext, ResolverFn, RouterDef as RouterDef2, RouterRoutes, router } from "@sylphx/lens-core";
2
+ import { ContextValue, EntityDef, InferRouterContext, MutationDef, QueryDef, Resolvers, RouterDef } from "@sylphx/lens-core";
3
3
  import { ArrayOperation, EmitCommand, EntityKey, InternalFieldUpdate, Update } from "@sylphx/lens-core";
4
4
  /** Client connection interface */
5
5
  interface StateClient {
@@ -217,17 +217,21 @@ type EntitiesMap = Record<string, EntityDef<string, any>>;
217
217
  type QueriesMap = Record<string, QueryDef<unknown, unknown>>;
218
218
  /** Mutations map type */
219
219
  type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
220
- /** Relations array type */
221
- type RelationsArray = RelationDef<EntityDef<string, EntityDefinition>, Record<string, RelationTypeWithForeignKey>>[];
222
220
  /** Operation metadata for handshake */
223
221
  interface OperationMeta {
224
- type: "query" | "mutation";
222
+ type: "query" | "mutation" | "subscription";
225
223
  optimistic?: unknown;
226
224
  }
227
225
  /** Nested operations structure for handshake */
228
226
  type OperationsMap = {
229
227
  [key: string]: OperationMeta | OperationsMap;
230
228
  };
229
+ /** Logger interface for server */
230
+ interface LensLogger {
231
+ info?: (message: string, ...args: unknown[]) => void;
232
+ warn?: (message: string, ...args: unknown[]) => void;
233
+ error?: (message: string, ...args: unknown[]) => void;
234
+ }
231
235
  /** Server configuration */
232
236
  interface LensServerConfig<
233
237
  TContext extends ContextValue = ContextValue,
@@ -235,16 +239,16 @@ interface LensServerConfig<
235
239
  > {
236
240
  /** Entity definitions */
237
241
  entities?: EntitiesMap;
238
- /** Relation definitions */
239
- relations?: RelationsArray;
240
242
  /** Router definition (namespaced operations) - context type is inferred */
241
243
  router?: TRouter;
242
244
  /** Query definitions (flat, legacy) */
243
245
  queries?: QueriesMap;
244
246
  /** Mutation definitions (flat, legacy) */
245
247
  mutations?: MutationsMap;
246
- /** Entity resolvers */
247
- resolvers?: EntityResolvers<EntityResolversDefinition>;
248
+ /** Field resolvers array (use lens() factory to create) */
249
+ resolvers?: Resolvers;
250
+ /** Logger for server messages (default: silent) */
251
+ logger?: LensLogger;
248
252
  /** Context factory - must return the context type expected by the router */
249
253
  context?: (req?: unknown) => TContext | Promise<TContext>;
250
254
  /** Server version */
@@ -308,96 +312,6 @@ interface WebSocketLike {
308
312
  onclose?: (() => void) | null;
309
313
  onerror?: ((error: unknown) => void) | null;
310
314
  }
311
- declare class LensServerImpl<
312
- Q extends QueriesMap = QueriesMap,
313
- M extends MutationsMap = MutationsMap,
314
- TContext extends ContextValue = ContextValue
315
- > implements LensServer {
316
- private queries;
317
- private mutations;
318
- private entities;
319
- private resolvers?;
320
- private contextFactory;
321
- private version;
322
- private ctx;
323
- /** GraphStateManager for per-client state tracking */
324
- private stateManager;
325
- /** DataLoaders for N+1 batching (per-request) */
326
- private loaders;
327
- /** Client connections */
328
- private connections;
329
- private connectionCounter;
330
- /** Server instance */
331
- private server;
332
- constructor(config: LensServerConfig<TContext> & {
333
- queries?: Q;
334
- mutations?: M;
335
- });
336
- getStateManager(): GraphStateManager;
337
- /**
338
- * Get server metadata for transport handshake.
339
- * Used by inProcess transport for direct access.
340
- */
341
- getMetadata(): ServerMetadata;
342
- /**
343
- * Execute operation - auto-detects query vs mutation from registered operations.
344
- * Used by inProcess transport for direct server calls.
345
- */
346
- execute(op: LensOperation): Promise<LensResult>;
347
- /**
348
- * Build nested operations map for handshake response
349
- * Converts flat "user.get", "user.create" into nested { user: { get: {...}, create: {...} } }
350
- */
351
- private buildOperationsMap;
352
- handleWebSocket(ws: WebSocketLike): void;
353
- private handleMessage;
354
- private handleHandshake;
355
- private handleSubscribe;
356
- private executeSubscription;
357
- private handleUpdateFields;
358
- private handleUnsubscribe;
359
- private handleQuery;
360
- private handleMutation;
361
- private handleDisconnect;
362
- executeQuery<
363
- TInput,
364
- TOutput
365
- >(name: string, input?: TInput): Promise<TOutput>;
366
- executeMutation<
367
- TInput,
368
- TOutput
369
- >(name: string, input: TInput): Promise<TOutput>;
370
- handleRequest(req: Request): Promise<Response>;
371
- listen(port: number): Promise<void>;
372
- close(): Promise<void>;
373
- private findConnectionByWs;
374
- private getEntityNameFromOutput;
375
- private getEntityNameFromMutation;
376
- private extractEntities;
377
- private applySelection;
378
- private applySelectionToObject;
379
- /**
380
- * Execute entity resolvers for nested data.
381
- * Processes the selection object and resolves relation fields.
382
- */
383
- private executeEntityResolvers;
384
- /**
385
- * Get target entity name for a relation field.
386
- */
387
- private getRelationTargetEntity;
388
- /**
389
- * Serialize entity data for transport.
390
- * Auto-calls serialize() on field types (Date → ISO string, etc.)
391
- */
392
- private serializeEntity;
393
- /**
394
- * Process query result: execute entity resolvers, apply selection, serialize
395
- */
396
- private processQueryResult;
397
- private computeUpdates;
398
- private deepEqual;
399
- private clearLoaders;
400
- }
401
315
  /**
402
316
  * Infer input type from a query/mutation definition
403
317
  */
@@ -421,10 +335,9 @@ type InferOutput<T> = T extends QueryDef<unknown, infer O> ? O : T extends Mutat
421
335
  * const client = createClient<Api>({ links: [...] });
422
336
  * ```
423
337
  */
424
- type InferApi<T extends LensServer> = T extends LensServerImpl<infer Q, infer M> ? {
425
- queries: Q;
426
- mutations: M;
427
- } : never;
338
+ type InferApi<T> = T extends {
339
+ _types: infer Types;
340
+ } ? Types : never;
428
341
  /**
429
342
  * Config helper type that infers context from router
430
343
  */
@@ -434,11 +347,11 @@ type ServerConfigWithInferredContext<
434
347
  M extends MutationsMap = MutationsMap
435
348
  > = {
436
349
  entities?: EntitiesMap;
437
- relations?: RelationsArray;
438
350
  router: TRouter;
439
351
  queries?: Q;
440
352
  mutations?: M;
441
- resolvers?: EntityResolvers<EntityResolversDefinition>;
353
+ /** Field resolvers array */
354
+ resolvers?: Resolvers;
442
355
  /** Context factory - type is inferred from router's procedures */
443
356
  context?: (req?: unknown) => InferRouterContext<TRouter> | Promise<InferRouterContext<TRouter>>;
444
357
  version?: string;
@@ -452,11 +365,11 @@ type ServerConfigLegacy<
452
365
  M extends MutationsMap = MutationsMap
453
366
  > = {
454
367
  entities?: EntitiesMap;
455
- relations?: RelationsArray;
456
368
  router?: undefined;
457
369
  queries?: Q;
458
370
  mutations?: M;
459
- resolvers?: EntityResolvers<EntityResolversDefinition>;
371
+ /** Field resolvers array */
372
+ resolvers?: Resolvers;
460
373
  context?: (req?: unknown) => TContext | Promise<TContext>;
461
374
  version?: string;
462
375
  };
@@ -484,6 +397,7 @@ declare function createServer<
484
397
  M extends MutationsMap = MutationsMap
485
398
  >(config: ServerConfigWithInferredContext<TRouter, Q, M>): LensServer & {
486
399
  _types: {
400
+ router: TRouter;
487
401
  queries: Q;
488
402
  mutations: M;
489
403
  context: InferRouterContext<TRouter>;
@@ -544,7 +458,7 @@ declare class SSEHandler {
544
458
  * Handle new SSE connection
545
459
  * Returns a Response with SSE stream
546
460
  */
547
- handleConnection(req?: Request): Response;
461
+ handleConnection(_req?: Request): Response;
548
462
  /**
549
463
  * Remove client and cleanup
550
464
  */
@@ -570,4 +484,4 @@ declare class SSEHandler {
570
484
  * Create SSE handler (transport adapter)
571
485
  */
572
486
  declare function createSSEHandler(config: SSEHandlerConfig): SSEHandler;
573
- export { router, query, mutation, createServer, createSSEHandler, createGraphStateManager, WebSocketLike, Subscription, StateUpdateMessage, StateFullMessage, StateClient, ServerMetadata, LensServerConfig as ServerConfig, SelectionObject, SSEHandlerConfig, SSEHandler, SSEClientInfo, RouterRoutes, RouterDef2 as RouterDef, ResolverFn, ResolverContext, RelationsArray, QueryDef2 as QueryDef, QueryBuilder, QueriesMap, OperationsMap, OperationMeta, MutationsMap, MutationDef2 as MutationDef, MutationBuilder, LensServer, LensResult, LensOperation, InferRouterContext2 as InferRouterContext, InferOutput, InferInput, InferApi, GraphStateManagerConfig, GraphStateManager, EntityKey, EntitiesMap };
487
+ export { router, query, mutation, createServer, createSSEHandler, createGraphStateManager, WebSocketLike, Subscription, StateUpdateMessage, StateFullMessage, StateClient, ServerMetadata, LensServerConfig as ServerConfig, SelectionObject, SSEHandlerConfig, SSEHandler, SSEClientInfo, RouterRoutes, RouterDef2 as RouterDef, ResolverFn, ResolverContext, QueryDef2 as QueryDef, QueriesMap, OperationsMap, OperationMeta, MutationsMap, MutationDef2 as MutationDef, LensServer, LensResult, LensOperation, InferRouterContext2 as InferRouterContext, InferOutput, InferInput, InferApi, GraphStateManagerConfig, GraphStateManager, EntityKey, EntitiesMap };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import {
3
- query,
4
3
  mutation,
4
+ query,
5
5
  router
6
6
  } from "@sylphx/lens-core";
7
7
 
@@ -11,15 +11,16 @@ import {
11
11
  createEmit,
12
12
  createUpdate as createUpdate2,
13
13
  flattenRouter,
14
- isBatchResolver,
15
14
  isMutationDef,
16
15
  isQueryDef,
17
- runWithContext
16
+ runWithContext,
17
+ toResolverMap
18
18
  } from "@sylphx/lens-core";
19
19
 
20
20
  // src/state/graph-state-manager.ts
21
21
  import {
22
22
  applyUpdate,
23
+ computeArrayDiff,
23
24
  createUpdate,
24
25
  makeEntityKey
25
26
  } from "@sylphx/lens-core";
@@ -241,14 +242,35 @@ class GraphStateManager {
241
242
  if (JSON.stringify(lastState) === JSON.stringify(newArray)) {
242
243
  return;
243
244
  }
244
- client.send({
245
- type: "update",
246
- entity,
247
- id,
248
- updates: {
249
- _items: { strategy: "value", data: newArray }
250
- }
251
- });
245
+ const diff = computeArrayDiff(lastState, newArray);
246
+ if (diff === null || diff.length === 0) {
247
+ client.send({
248
+ type: "update",
249
+ entity,
250
+ id,
251
+ updates: {
252
+ _items: { strategy: "value", data: newArray }
253
+ }
254
+ });
255
+ } else if (diff.length === 1 && diff[0].op === "replace") {
256
+ client.send({
257
+ type: "update",
258
+ entity,
259
+ id,
260
+ updates: {
261
+ _items: { strategy: "value", data: newArray }
262
+ }
263
+ });
264
+ } else {
265
+ client.send({
266
+ type: "update",
267
+ entity,
268
+ id,
269
+ updates: {
270
+ _items: { strategy: "array", data: diff }
271
+ }
272
+ });
273
+ }
252
274
  clientArrayState.lastState = [...newArray];
253
275
  }
254
276
  getArrayState(entity, id) {
@@ -471,11 +493,13 @@ class DataLoader {
471
493
  keys.forEach((key, index) => {
472
494
  const callbacks = batch.get(key);
473
495
  const result = results[index] ?? null;
474
- callbacks.forEach(({ resolve }) => resolve(result));
496
+ for (const { resolve } of callbacks)
497
+ resolve(result);
475
498
  });
476
499
  } catch (error) {
477
500
  for (const callbacks of batch.values()) {
478
- callbacks.forEach(({ reject }) => reject(error));
501
+ for (const { reject } of callbacks)
502
+ reject(error);
479
503
  }
480
504
  }
481
505
  }
@@ -483,14 +507,16 @@ class DataLoader {
483
507
  this.batch.clear();
484
508
  }
485
509
  }
510
+ var noopLogger = {};
486
511
 
487
512
  class LensServerImpl {
488
513
  queries;
489
514
  mutations;
490
515
  entities;
491
- resolvers;
516
+ resolverMap;
492
517
  contextFactory;
493
518
  version;
519
+ logger;
494
520
  ctx = createContext();
495
521
  stateManager;
496
522
  loaders = new Map;
@@ -513,9 +539,10 @@ class LensServerImpl {
513
539
  this.queries = queries;
514
540
  this.mutations = mutations;
515
541
  this.entities = config.entities ?? {};
516
- this.resolvers = config.resolvers;
542
+ this.resolverMap = config.resolvers ? toResolverMap(config.resolvers) : undefined;
517
543
  this.contextFactory = config.context ?? (() => ({}));
518
544
  this.version = config.version ?? "1.0.0";
545
+ this.logger = config.logger ?? noopLogger;
519
546
  for (const [name, def] of Object.entries(this.entities)) {
520
547
  if (def && typeof def === "object" && !def._name) {
521
548
  def._name = name;
@@ -594,7 +621,7 @@ class LensServerImpl {
594
621
  }
595
622
  current[parts[parts.length - 1]] = meta;
596
623
  };
597
- for (const [name, def] of Object.entries(this.queries)) {
624
+ for (const [name, _def] of Object.entries(this.queries)) {
598
625
  setNested(name, { type: "query" });
599
626
  }
600
627
  for (const [name, def] of Object.entries(this.mutations)) {
@@ -820,7 +847,7 @@ class LensServerImpl {
820
847
  try {
821
848
  cleanup();
822
849
  } catch (e) {
823
- console.error("Cleanup error:", e);
850
+ this.logger.error?.("Cleanup error:", e);
824
851
  }
825
852
  }
826
853
  for (const entityKey of sub.entityKeys) {
@@ -877,7 +904,7 @@ class LensServerImpl {
877
904
  try {
878
905
  cleanup();
879
906
  } catch (e) {
880
- console.error("Cleanup error:", e);
907
+ this.logger.error?.("Cleanup error:", e);
881
908
  }
882
909
  }
883
910
  }
@@ -1031,7 +1058,7 @@ class LensServerImpl {
1031
1058
  }
1032
1059
  }
1033
1060
  });
1034
- console.log(`Lens server listening on port ${port}`);
1061
+ this.logger.info?.(`Lens server listening on port ${port}`);
1035
1062
  }
1036
1063
  async close() {
1037
1064
  if (this.server && typeof this.server.stop === "function") {
@@ -1144,27 +1171,20 @@ class LensServerImpl {
1144
1171
  return result;
1145
1172
  }
1146
1173
  async executeEntityResolvers(entityName, data, select) {
1147
- if (!data || !select || !this.resolvers)
1174
+ if (!data || !select || !this.resolverMap)
1175
+ return data;
1176
+ const resolverDef = this.resolverMap.get(entityName);
1177
+ if (!resolverDef)
1148
1178
  return data;
1149
1179
  const result = { ...data };
1180
+ const context = await this.contextFactory();
1150
1181
  for (const [fieldName, fieldSelect] of Object.entries(select)) {
1151
1182
  if (fieldSelect === false || fieldSelect === true)
1152
1183
  continue;
1153
- const resolver = this.resolvers.getResolver(entityName, fieldName);
1154
- if (!resolver)
1184
+ if (!resolverDef.hasField(fieldName))
1155
1185
  continue;
1156
- if (isBatchResolver(resolver)) {
1157
- const loaderKey = `${entityName}.${fieldName}`;
1158
- if (!this.loaders.has(loaderKey)) {
1159
- this.loaders.set(loaderKey, new DataLoader(async (parents) => {
1160
- return resolver.batch(parents);
1161
- }));
1162
- }
1163
- const loader = this.loaders.get(loaderKey);
1164
- result[fieldName] = await loader.load(data);
1165
- } else {
1166
- result[fieldName] = await resolver(data);
1167
- }
1186
+ const fieldArgs = typeof fieldSelect === "object" && fieldSelect !== null && "args" in fieldSelect ? fieldSelect.args ?? {} : {};
1187
+ result[fieldName] = await resolverDef.resolveField(fieldName, data, fieldArgs, context);
1168
1188
  const nestedSelect = fieldSelect.select;
1169
1189
  if (nestedSelect && result[fieldName]) {
1170
1190
  const relationData = result[fieldName];
@@ -1228,7 +1248,7 @@ class LensServerImpl {
1228
1248
  try {
1229
1249
  result[fieldName] = fieldType.serialize(value);
1230
1250
  } catch (error) {
1231
- console.warn(`Failed to serialize field ${entityName}.${fieldName}:`, error);
1251
+ this.logger.warn?.(`Failed to serialize field ${entityName}.${fieldName}:`, error);
1232
1252
  result[fieldName] = value;
1233
1253
  }
1234
1254
  } else {
@@ -1245,7 +1265,7 @@ class LensServerImpl {
1245
1265
  if (Array.isArray(data)) {
1246
1266
  const processedItems = await Promise.all(data.map(async (item) => {
1247
1267
  let result2 = item;
1248
- if (select && this.resolvers) {
1268
+ if (select && this.resolverMap) {
1249
1269
  result2 = await this.executeEntityResolvers(entityName, item, select);
1250
1270
  }
1251
1271
  if (select) {
@@ -1259,7 +1279,7 @@ class LensServerImpl {
1259
1279
  return processedItems;
1260
1280
  }
1261
1281
  let result = data;
1262
- if (select && this.resolvers) {
1282
+ if (select && this.resolverMap) {
1263
1283
  result = await this.executeEntityResolvers(entityName, data, select);
1264
1284
  }
1265
1285
  if (select) {
@@ -1330,7 +1350,7 @@ class SSEHandler {
1330
1350
  this.stateManager = config.stateManager;
1331
1351
  this.heartbeatInterval = config.heartbeatInterval ?? 30000;
1332
1352
  }
1333
- handleConnection(req) {
1353
+ handleConnection(_req) {
1334
1354
  const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
1335
1355
  const encoder = new TextEncoder;
1336
1356
  const stream = new ReadableStream({
package/package.json CHANGED
@@ -1,38 +1,39 @@
1
1
  {
2
- "name": "@sylphx/lens-server",
3
- "version": "1.3.2",
4
- "description": "Server runtime for Lens API framework",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
12
- }
13
- },
14
- "scripts": {
15
- "build": "bunup",
16
- "typecheck": "tsc --noEmit",
17
- "test": "bun test"
18
- },
19
- "files": [
20
- "dist",
21
- "src"
22
- ],
23
- "keywords": [
24
- "lens",
25
- "server",
26
- "resolvers",
27
- "graphql"
28
- ],
29
- "author": "SylphxAI",
30
- "license": "MIT",
31
- "dependencies": {
32
- "@sylphx/lens-core": "^1.3.2"
33
- },
34
- "devDependencies": {
35
- "typescript": "^5.9.3",
36
- "zod": "^4.1.13"
37
- }
2
+ "name": "@sylphx/lens-server",
3
+ "version": "1.5.1",
4
+ "description": "Server runtime for Lens API framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "bunup",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "bun test",
18
+ "prepack": "bun run build"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "keywords": [
25
+ "lens",
26
+ "server",
27
+ "resolvers",
28
+ "graphql"
29
+ ],
30
+ "author": "SylphxAI",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@sylphx/lens-core": "^1.10.0"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.9.3",
37
+ "zod": "^4.1.13"
38
+ }
38
39
  }