@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 CHANGED
@@ -1,10 +1,510 @@
1
+ import { ContextValue, EntityDef, EntityDefinition, EntityResolvers, EntityResolversDefinition, MutationDef, QueryDef, RelationDef, RelationTypeWithForeignKey, RouterDef } from "@sylphx/lens-core";
2
+ import { EntityKey, Update, EmitCommand, InternalFieldUpdate, ArrayOperation } from "@sylphx/lens-core";
3
+ /** Client connection interface */
4
+ interface StateClient {
5
+ id: string;
6
+ send: (message: StateUpdateMessage) => void;
7
+ }
8
+ /** Update message sent to clients */
9
+ interface StateUpdateMessage {
10
+ type: "update";
11
+ entity: string;
12
+ id: string;
13
+ /** Field-level updates with strategy */
14
+ updates: Record<string, Update>;
15
+ }
16
+ /** Full entity update message */
17
+ interface StateFullMessage {
18
+ type: "data";
19
+ entity: string;
20
+ id: string;
21
+ data: Record<string, unknown>;
22
+ }
23
+ /** Subscription info */
24
+ interface Subscription {
25
+ clientId: string;
26
+ fields: Set<string> | "*";
27
+ }
28
+ /** Configuration */
29
+ interface GraphStateManagerConfig {
30
+ /** Called when an entity has no more subscribers */
31
+ onEntityUnsubscribed?: (entity: string, id: string) => void;
32
+ }
1
33
  /**
2
- * @sylphx/lens-server
3
- *
4
- * Server runtime for Lens API framework.
5
- * Operations-based server with GraphStateManager for reactive updates.
6
- */
7
- export { createServer, type LensServer, type LensServerConfig as ServerConfig, type EntitiesMap, type RelationsArray, type QueriesMap, type MutationsMap, type WebSocketLike, type SelectionObject, type ServerMetadata, type OperationMeta, type OperationsMap, type LensOperation, type LensResult, type InferApi, type InferInput, type InferOutput, } from "./server/create";
8
- export { GraphStateManager, createGraphStateManager, type EntityKey, type StateClient, type StateUpdateMessage, type StateFullMessage, type Subscription, type GraphStateManagerConfig, } from "./state";
9
- export { SSEHandler, createSSEHandler, type SSEHandlerConfig, type SSEClientInfo, } from "./sse/handler";
10
- //# sourceMappingURL=index.d.ts.map
34
+ * Manages server-side canonical state and syncs to clients.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const manager = new GraphStateManager();
39
+ *
40
+ * // Add client
41
+ * manager.addClient({
42
+ * id: "client-1",
43
+ * send: (msg) => ws.send(JSON.stringify(msg)),
44
+ * });
45
+ *
46
+ * // Subscribe client to entity
47
+ * manager.subscribe("client-1", "Post", "123", ["title", "content"]);
48
+ *
49
+ * // Emit updates (from resolvers)
50
+ * manager.emit("Post", "123", { content: "Updated content" });
51
+ * // → Automatically computes diff and sends to subscribed clients
52
+ * ```
53
+ */
54
+ declare class GraphStateManager {
55
+ /** Connected clients */
56
+ private clients;
57
+ /** Canonical state per entity (server truth) */
58
+ private canonical;
59
+ /** Canonical array state per entity (server truth for array outputs) */
60
+ private canonicalArrays;
61
+ /** Per-client state tracking */
62
+ private clientStates;
63
+ /** Per-client array state tracking */
64
+ private clientArrayStates;
65
+ /** Entity → subscribed client IDs */
66
+ private entitySubscribers;
67
+ /** Configuration */
68
+ private config;
69
+ constructor(config?: GraphStateManagerConfig);
70
+ /**
71
+ * Add a client connection
72
+ */
73
+ addClient(client: StateClient): void;
74
+ /**
75
+ * Remove a client and cleanup all subscriptions
76
+ */
77
+ removeClient(clientId: string): void;
78
+ /**
79
+ * Subscribe a client to an entity
80
+ */
81
+ subscribe(clientId: string, entity: string, id: string, fields?: string[] | "*"): void;
82
+ /**
83
+ * Unsubscribe a client from an entity
84
+ */
85
+ unsubscribe(clientId: string, entity: string, id: string): void;
86
+ /**
87
+ * Update subscription fields for a client
88
+ */
89
+ updateSubscription(clientId: string, entity: string, id: string, fields: string[] | "*"): void;
90
+ /**
91
+ * Emit data for an entity.
92
+ * This is the core method called by resolvers.
93
+ *
94
+ * @param entity - Entity name
95
+ * @param id - Entity ID
96
+ * @param data - Full or partial entity data
97
+ * @param options - Emit options
98
+ */
99
+ emit(entity: string, id: string, data: Record<string, unknown>, options?: {
100
+ replace?: boolean;
101
+ }): void;
102
+ /**
103
+ * Emit a field-level update with a specific strategy.
104
+ * Applies the update to canonical state and pushes to clients.
105
+ *
106
+ * @param entity - Entity name
107
+ * @param id - Entity ID
108
+ * @param field - Field name to update
109
+ * @param update - Update with strategy (value/delta/patch)
110
+ */
111
+ emitField(entity: string, id: string, field: string, update: Update): void;
112
+ /**
113
+ * Emit multiple field updates in a batch.
114
+ * More efficient than multiple emitField calls.
115
+ *
116
+ * @param entity - Entity name
117
+ * @param id - Entity ID
118
+ * @param updates - Array of field updates
119
+ */
120
+ emitBatch(entity: string, id: string, updates: InternalFieldUpdate[]): void;
121
+ /**
122
+ * Process an EmitCommand from the Emit API.
123
+ * Routes to appropriate emit method.
124
+ *
125
+ * @param entity - Entity name
126
+ * @param id - Entity ID
127
+ * @param command - Emit command from resolver
128
+ */
129
+ processCommand(entity: string, id: string, command: EmitCommand): void;
130
+ /**
131
+ * Emit array data (replace entire array).
132
+ *
133
+ * @param entity - Entity name
134
+ * @param id - Entity ID
135
+ * @param items - Array items
136
+ */
137
+ emitArray(entity: string, id: string, items: unknown[]): void;
138
+ /**
139
+ * Apply an array operation to the canonical state.
140
+ *
141
+ * @param entity - Entity name
142
+ * @param id - Entity ID
143
+ * @param operation - Array operation to apply
144
+ */
145
+ emitArrayOperation(entity: string, id: string, operation: ArrayOperation): void;
146
+ /**
147
+ * Apply an array operation and return new array.
148
+ */
149
+ private applyArrayOperation;
150
+ /**
151
+ * Push array update to a specific client.
152
+ * Computes optimal diff strategy.
153
+ */
154
+ private pushArrayToClient;
155
+ /**
156
+ * Get current canonical array state
157
+ */
158
+ getArrayState(entity: string, id: string): unknown[] | undefined;
159
+ /**
160
+ * Get current canonical state for an entity
161
+ */
162
+ getState(entity: string, id: string): Record<string, unknown> | undefined;
163
+ /**
164
+ * Check if entity has any subscribers
165
+ */
166
+ hasSubscribers(entity: string, id: string): boolean;
167
+ /**
168
+ * Push update to a specific client
169
+ */
170
+ private pushToClient;
171
+ /**
172
+ * Push a single field update to a client.
173
+ * Computes optimal transfer strategy.
174
+ */
175
+ private pushFieldToClient;
176
+ /**
177
+ * Push multiple field updates to a client.
178
+ * Computes optimal transfer strategy for each field.
179
+ */
180
+ private pushFieldsToClient;
181
+ /**
182
+ * Send initial data to a newly subscribed client
183
+ */
184
+ private sendInitialData;
185
+ /**
186
+ * Cleanup entity when no subscribers remain
187
+ */
188
+ private cleanupEntity;
189
+ private makeKey;
190
+ /**
191
+ * Get statistics
192
+ */
193
+ getStats(): {
194
+ clients: number;
195
+ entities: number;
196
+ totalSubscriptions: number;
197
+ };
198
+ /**
199
+ * Clear all state (for testing)
200
+ */
201
+ clear(): void;
202
+ }
203
+ /**
204
+ * Create a GraphStateManager instance
205
+ */
206
+ declare function createGraphStateManager(config?: GraphStateManagerConfig): GraphStateManager;
207
+ /** Selection object type for nested field selection */
208
+ interface SelectionObject {
209
+ [key: string]: boolean | SelectionObject | {
210
+ select: SelectionObject;
211
+ };
212
+ }
213
+ /** Entity map type */
214
+ type EntitiesMap = Record<string, EntityDef<string, any>>;
215
+ /** Queries map type */
216
+ type QueriesMap = Record<string, QueryDef<unknown, unknown>>;
217
+ /** Mutations map type */
218
+ type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
219
+ /** Relations array type */
220
+ type RelationsArray = RelationDef<EntityDef<string, EntityDefinition>, Record<string, RelationTypeWithForeignKey>>[];
221
+ /** Operation metadata for handshake */
222
+ interface OperationMeta {
223
+ type: "query" | "mutation";
224
+ optimistic?: unknown;
225
+ }
226
+ /** Nested operations structure for handshake */
227
+ type OperationsMap = {
228
+ [key: string]: OperationMeta | OperationsMap;
229
+ };
230
+ /** Server configuration */
231
+ interface LensServerConfig<TContext extends ContextValue = ContextValue> {
232
+ /** Entity definitions */
233
+ entities?: EntitiesMap;
234
+ /** Relation definitions */
235
+ relations?: RelationsArray;
236
+ /** Router definition (namespaced operations) */
237
+ router?: RouterDef;
238
+ /** Query definitions (flat, legacy) */
239
+ queries?: QueriesMap;
240
+ /** Mutation definitions (flat, legacy) */
241
+ mutations?: MutationsMap;
242
+ /** Entity resolvers */
243
+ resolvers?: EntityResolvers<EntityResolversDefinition>;
244
+ /** Context factory */
245
+ context?: (req?: unknown) => TContext | Promise<TContext>;
246
+ /** Server version */
247
+ version?: string;
248
+ }
249
+ /** Server metadata for transport handshake */
250
+ interface ServerMetadata {
251
+ /** Server version */
252
+ version: string;
253
+ /** Operations metadata map */
254
+ operations: OperationsMap;
255
+ }
256
+ /** Operation for in-process transport */
257
+ interface LensOperation {
258
+ /** Operation path (e.g., 'user.get', 'session.create') */
259
+ path: string;
260
+ /** Operation input */
261
+ input?: unknown;
262
+ }
263
+ /** Result from operation execution */
264
+ interface LensResult<T = unknown> {
265
+ /** Success data */
266
+ data?: T;
267
+ /** Error if operation failed */
268
+ error?: Error;
269
+ }
270
+ /** Lens server interface */
271
+ interface LensServer {
272
+ /** Get server metadata for transport handshake */
273
+ getMetadata(): ServerMetadata;
274
+ /** Execute operation - auto-detects query vs mutation from registered operations */
275
+ execute(op: LensOperation): Promise<LensResult>;
276
+ /** Execute a query (one-time) */
277
+ executeQuery<
278
+ TInput,
279
+ TOutput
280
+ >(name: string, input?: TInput): Promise<TOutput>;
281
+ /** Execute a mutation */
282
+ executeMutation<
283
+ TInput,
284
+ TOutput
285
+ >(name: string, input: TInput): Promise<TOutput>;
286
+ /** Handle WebSocket connection */
287
+ handleWebSocket(ws: WebSocketLike): void;
288
+ /** Handle HTTP request */
289
+ handleRequest(req: Request): Promise<Response>;
290
+ /** Get GraphStateManager for external access */
291
+ getStateManager(): GraphStateManager;
292
+ /** Start server */
293
+ listen(port: number): Promise<void>;
294
+ /** Close server */
295
+ close(): Promise<void>;
296
+ }
297
+ /** WebSocket interface */
298
+ interface WebSocketLike {
299
+ send(data: string): void;
300
+ close(): void;
301
+ onmessage?: ((event: {
302
+ data: string;
303
+ }) => void) | null;
304
+ onclose?: (() => void) | null;
305
+ onerror?: ((error: unknown) => void) | null;
306
+ }
307
+ declare class LensServerImpl<
308
+ Q extends QueriesMap = QueriesMap,
309
+ M extends MutationsMap = MutationsMap,
310
+ TContext extends ContextValue = ContextValue
311
+ > implements LensServer {
312
+ private queries;
313
+ private mutations;
314
+ private entities;
315
+ private resolvers?;
316
+ private contextFactory;
317
+ private version;
318
+ private ctx;
319
+ /** GraphStateManager for per-client state tracking */
320
+ private stateManager;
321
+ /** DataLoaders for N+1 batching (per-request) */
322
+ private loaders;
323
+ /** Client connections */
324
+ private connections;
325
+ private connectionCounter;
326
+ /** Server instance */
327
+ private server;
328
+ constructor(config: LensServerConfig<TContext> & {
329
+ queries?: Q;
330
+ mutations?: M;
331
+ });
332
+ getStateManager(): GraphStateManager;
333
+ /**
334
+ * Get server metadata for transport handshake.
335
+ * Used by inProcess transport for direct access.
336
+ */
337
+ getMetadata(): ServerMetadata;
338
+ /**
339
+ * Execute operation - auto-detects query vs mutation from registered operations.
340
+ * Used by inProcess transport for direct server calls.
341
+ */
342
+ execute(op: LensOperation): Promise<LensResult>;
343
+ /**
344
+ * Build nested operations map for handshake response
345
+ * Converts flat "user.get", "user.create" into nested { user: { get: {...}, create: {...} } }
346
+ */
347
+ private buildOperationsMap;
348
+ handleWebSocket(ws: WebSocketLike): void;
349
+ private handleMessage;
350
+ private handleHandshake;
351
+ private handleSubscribe;
352
+ private executeSubscription;
353
+ private handleUpdateFields;
354
+ private handleUnsubscribe;
355
+ private handleQuery;
356
+ private handleMutation;
357
+ private handleDisconnect;
358
+ executeQuery<
359
+ TInput,
360
+ TOutput
361
+ >(name: string, input?: TInput): Promise<TOutput>;
362
+ executeMutation<
363
+ TInput,
364
+ TOutput
365
+ >(name: string, input: TInput): Promise<TOutput>;
366
+ handleRequest(req: Request): Promise<Response>;
367
+ listen(port: number): Promise<void>;
368
+ close(): Promise<void>;
369
+ private findConnectionByWs;
370
+ private getEntityNameFromOutput;
371
+ private getEntityNameFromMutation;
372
+ private extractEntities;
373
+ private applySelection;
374
+ private applySelectionToObject;
375
+ /**
376
+ * Execute entity resolvers for nested data.
377
+ * Processes the selection object and resolves relation fields.
378
+ */
379
+ private executeEntityResolvers;
380
+ /**
381
+ * Get target entity name for a relation field.
382
+ */
383
+ private getRelationTargetEntity;
384
+ /**
385
+ * Serialize entity data for transport.
386
+ * Auto-calls serialize() on field types (Date → ISO string, etc.)
387
+ */
388
+ private serializeEntity;
389
+ /**
390
+ * Process query result: execute entity resolvers, apply selection, serialize
391
+ */
392
+ private processQueryResult;
393
+ private computeUpdates;
394
+ private deepEqual;
395
+ private clearLoaders;
396
+ }
397
+ /**
398
+ * Infer input type from a query/mutation definition
399
+ */
400
+ type InferInput<T> = T extends QueryDef<infer I, unknown> ? I extends void ? void : I : T extends MutationDef<infer I, unknown> ? I : never;
401
+ /**
402
+ * Infer output type from a query/mutation definition
403
+ */
404
+ type InferOutput<T> = T extends QueryDef<unknown, infer O> ? O : T extends MutationDef<unknown, infer O> ? O : never;
405
+ /**
406
+ * API type for client inference
407
+ * Export this type for client-side type safety
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * // Server
412
+ * const server = createLensServer({ queries, mutations });
413
+ * type Api = InferApi<typeof server>;
414
+ *
415
+ * // Client (only imports TYPE)
416
+ * import type { Api } from './server';
417
+ * const client = createClient<Api>({ links: [...] });
418
+ * ```
419
+ */
420
+ type InferApi<T extends LensServer> = T extends LensServerImpl<infer Q, infer M> ? {
421
+ queries: Q;
422
+ mutations: M;
423
+ } : never;
424
+ /**
425
+ * Create Lens server with Operations API + Optimization Layer
426
+ */
427
+ declare function createServer<
428
+ TContext extends ContextValue = ContextValue,
429
+ Q extends QueriesMap = QueriesMap,
430
+ M extends MutationsMap = MutationsMap
431
+ >(config: LensServerConfig<TContext> & {
432
+ queries?: Q;
433
+ mutations?: M;
434
+ }): LensServer & {
435
+ _types: {
436
+ queries: Q;
437
+ mutations: M;
438
+ };
439
+ };
440
+ /** SSE handler configuration */
441
+ interface SSEHandlerConfig {
442
+ /** GraphStateManager instance (required) */
443
+ stateManager: GraphStateManager;
444
+ /** Heartbeat interval in ms (default: 30000) */
445
+ heartbeatInterval?: number;
446
+ }
447
+ /** SSE client info */
448
+ interface SSEClientInfo {
449
+ id: string;
450
+ connectedAt: number;
451
+ }
452
+ /**
453
+ * SSE transport adapter for GraphStateManager.
454
+ *
455
+ * This is a thin adapter that:
456
+ * - Creates SSE connections
457
+ * - Registers clients with GraphStateManager
458
+ * - Forwards updates to SSE streams
459
+ *
460
+ * All state/subscription logic is handled by GraphStateManager.
461
+ *
462
+ * @example
463
+ * ```typescript
464
+ * const stateManager = new GraphStateManager();
465
+ * const sse = new SSEHandler({ stateManager });
466
+ *
467
+ * // Handle SSE connection
468
+ * app.get('/events', (req) => sse.handleConnection(req));
469
+ *
470
+ * // Subscribe via separate endpoint or message
471
+ * stateManager.subscribe(clientId, "Post", "123", "*");
472
+ * ```
473
+ */
474
+ declare class SSEHandler {
475
+ private stateManager;
476
+ private heartbeatInterval;
477
+ private clients;
478
+ private clientCounter;
479
+ constructor(config: SSEHandlerConfig);
480
+ /**
481
+ * Handle new SSE connection
482
+ * Returns a Response with SSE stream
483
+ */
484
+ handleConnection(req?: Request): Response;
485
+ /**
486
+ * Remove client and cleanup
487
+ */
488
+ private removeClient;
489
+ /**
490
+ * Close specific client connection
491
+ */
492
+ closeClient(clientId: string): void;
493
+ /**
494
+ * Get connected client count
495
+ */
496
+ getClientCount(): number;
497
+ /**
498
+ * Get connected client IDs
499
+ */
500
+ getClientIds(): string[];
501
+ /**
502
+ * Close all connections
503
+ */
504
+ closeAll(): void;
505
+ }
506
+ /**
507
+ * Create SSE handler (transport adapter)
508
+ */
509
+ declare function createSSEHandler(config: SSEHandlerConfig): SSEHandler;
510
+ export { createServer, createSSEHandler, createGraphStateManager, WebSocketLike, Subscription, StateUpdateMessage, StateFullMessage, StateClient, ServerMetadata, LensServerConfig as ServerConfig, SelectionObject, SSEHandlerConfig, SSEHandler, SSEClientInfo, RelationsArray, QueriesMap, OperationsMap, OperationMeta, MutationsMap, LensServer, LensResult, LensOperation, InferOutput, InferInput, InferApi, GraphStateManagerConfig, GraphStateManager, EntityKey, EntitiesMap };