@sylphx/lens-server 1.11.3 → 2.1.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,215 +1,462 @@
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
- import { ArrayOperation, EmitCommand, EntityKey, InternalFieldUpdate, Update } from "@sylphx/lens-core";
4
- /** Client connection interface */
5
- interface StateClient {
6
- id: string;
7
- send: (message: StateUpdateMessage) => void;
1
+ import { InferRouterContext as InferRouterContext3, MutationDef as MutationDef2, mutation, QueryDef as QueryDef2, query, ResolverContext, ResolverFn, RouterDef as RouterDef3, RouterRoutes, router } from "@sylphx/lens-core";
2
+ import { ContextStore, ContextValue } from "@sylphx/lens-core";
3
+ /**
4
+ * Create a typed context reference.
5
+ * This doesn't create a new AsyncLocalStorage, but provides type information.
6
+ */
7
+ declare function createContext<T extends ContextValue>(): ContextStore<T>;
8
+ /**
9
+ * Get the current context value.
10
+ * Throws if called outside of runWithContext.
11
+ */
12
+ declare function useContext<T extends ContextValue = ContextValue>(): T;
13
+ /**
14
+ * Try to get the current context value.
15
+ * Returns undefined if called outside of runWithContext.
16
+ */
17
+ declare function tryUseContext<T extends ContextValue = ContextValue>(): T | undefined;
18
+ /**
19
+ * Run a function with the given context.
20
+ */
21
+ declare function runWithContext<
22
+ T extends ContextValue,
23
+ R
24
+ >(_context: ContextStore<T>, value: T, fn: () => R): R;
25
+ /**
26
+ * Run an async function with the given context.
27
+ */
28
+ declare function runWithContextAsync<
29
+ T extends ContextValue,
30
+ R
31
+ >(context: ContextStore<T>, value: T, fn: () => Promise<R>): Promise<R>;
32
+ /**
33
+ * Check if currently running within a context.
34
+ */
35
+ declare function hasContext(): boolean;
36
+ /**
37
+ * Extend the current context with additional values.
38
+ */
39
+ declare function extendContext<
40
+ T extends ContextValue,
41
+ E extends ContextValue
42
+ >(current: T, extension: E): T & E;
43
+ import { ContextValue as ContextValue3, InferRouterContext as InferRouterContext2, RouterDef as RouterDef2 } from "@sylphx/lens-core";
44
+ import { ContextValue as ContextValue2, EntityDef, InferRouterContext, MutationDef, OptimisticDSL, QueryDef, Resolvers, RouterDef } from "@sylphx/lens-core";
45
+ /**
46
+ * @sylphx/lens-server - Plugin System Types
47
+ *
48
+ * Server-side plugin system with lifecycle hooks.
49
+ * Plugins can intercept, modify, or extend server behavior.
50
+ */
51
+ /**
52
+ * Context passed to onSubscribe hook.
53
+ */
54
+ interface SubscribeContext2 {
55
+ /** Client ID */
56
+ clientId: string;
57
+ /** Subscription ID (unique per client) */
58
+ subscriptionId: string;
59
+ /** Operation path (e.g., 'user.get') */
60
+ operation: string;
61
+ /** Operation input */
62
+ input: unknown;
63
+ /** Fields being subscribed to */
64
+ fields: string[] | "*";
65
+ /** Entity type (if determined) */
66
+ entity?: string;
67
+ /** Entity ID (if determined) */
68
+ entityId?: string;
69
+ }
70
+ /**
71
+ * Context passed to onUnsubscribe hook.
72
+ */
73
+ interface UnsubscribeContext2 {
74
+ /** Client ID */
75
+ clientId: string;
76
+ /** Subscription ID */
77
+ subscriptionId: string;
78
+ /** Operation path */
79
+ operation: string;
80
+ /** Entity keys that were being tracked */
81
+ entityKeys: string[];
82
+ }
83
+ /**
84
+ * Context passed to beforeSend hook.
85
+ *
86
+ * The beforeSend hook is the key integration point for optimization plugins.
87
+ * Plugins can intercept the data and return an optimized payload (e.g., diff).
88
+ */
89
+ interface BeforeSendContext {
90
+ /** Client ID */
91
+ clientId: string;
92
+ /** Subscription ID (unique per client subscription) */
93
+ subscriptionId: string;
94
+ /** Entity type */
95
+ entity: string;
96
+ /** Entity ID */
97
+ entityId: string;
98
+ /** Data to be sent (full entity data) */
99
+ data: Record<string, unknown>;
100
+ /** Whether this is the first send (initial subscription data) */
101
+ isInitial: boolean;
102
+ /** Fields the client is subscribed to */
103
+ fields: string[] | "*";
8
104
  }
9
- /** Update message sent to clients */
10
- interface StateUpdateMessage {
11
- type: "update";
105
+ /**
106
+ * Context passed to afterSend hook.
107
+ */
108
+ interface AfterSendContext {
109
+ /** Client ID */
110
+ clientId: string;
111
+ /** Subscription ID */
112
+ subscriptionId: string;
113
+ /** Entity type */
12
114
  entity: string;
13
- id: string;
14
- /** Field-level updates with strategy */
15
- updates: Record<string, Update>;
115
+ /** Entity ID */
116
+ entityId: string;
117
+ /** Data that was sent (may be optimized/transformed by beforeSend) */
118
+ data: Record<string, unknown>;
119
+ /** Whether this was the first send */
120
+ isInitial: boolean;
121
+ /** Fields the client is subscribed to */
122
+ fields: string[] | "*";
123
+ /** Timestamp of send */
124
+ timestamp: number;
125
+ }
126
+ /**
127
+ * Context passed to onConnect hook.
128
+ */
129
+ interface ConnectContext {
130
+ /** Client ID */
131
+ clientId: string;
132
+ /** Request object (if available) */
133
+ request?: Request;
134
+ /** Function to send messages to this client */
135
+ send?: (message: unknown) => void;
16
136
  }
17
- /** Full entity update message */
18
- interface StateFullMessage {
19
- type: "data";
137
+ /**
138
+ * Context passed to onBroadcast hook.
139
+ */
140
+ interface BroadcastContext {
141
+ /** Entity type name */
20
142
  entity: string;
21
- id: string;
143
+ /** Entity ID */
144
+ entityId: string;
145
+ /** Entity data */
22
146
  data: Record<string, unknown>;
23
147
  }
24
- /** Subscription info */
25
- interface Subscription {
148
+ /**
149
+ * Context passed to onDisconnect hook.
150
+ */
151
+ interface DisconnectContext {
152
+ /** Client ID */
153
+ clientId: string;
154
+ /** Number of active subscriptions at disconnect */
155
+ subscriptionCount: number;
156
+ }
157
+ /**
158
+ * Context passed to beforeMutation hook.
159
+ */
160
+ interface BeforeMutationContext {
161
+ /** Mutation name */
162
+ name: string;
163
+ /** Mutation input */
164
+ input: unknown;
165
+ /** Client ID (if from WebSocket) */
166
+ clientId?: string;
167
+ }
168
+ /**
169
+ * Context passed to afterMutation hook.
170
+ */
171
+ interface AfterMutationContext {
172
+ /** Mutation name */
173
+ name: string;
174
+ /** Mutation input */
175
+ input: unknown;
176
+ /** Mutation result */
177
+ result: unknown;
178
+ /** Client ID (if from WebSocket) */
179
+ clientId?: string;
180
+ /** Duration in milliseconds */
181
+ duration: number;
182
+ }
183
+ /**
184
+ * Context passed to onReconnect hook.
185
+ */
186
+ interface ReconnectContext2 {
187
+ /** Client ID */
188
+ clientId: string;
189
+ /** Subscriptions to restore */
190
+ subscriptions: Array<{
191
+ id: string;
192
+ entity: string;
193
+ entityId: string;
194
+ fields: string[] | "*";
195
+ version: number;
196
+ dataHash?: string;
197
+ input?: unknown;
198
+ }>;
199
+ /** Client-generated reconnect ID */
200
+ reconnectId: string;
201
+ }
202
+ /**
203
+ * Result from onReconnect hook.
204
+ */
205
+ interface ReconnectHookResult2 {
206
+ /** Subscription ID */
207
+ id: string;
208
+ /** Entity type */
209
+ entity: string;
210
+ /** Entity ID */
211
+ entityId: string;
212
+ /** Sync status */
213
+ status: "current" | "patched" | "snapshot" | "deleted" | "error";
214
+ /** Current server version */
215
+ version: number;
216
+ /** For "patched": ordered patches to apply */
217
+ patches?: Array<Array<{
218
+ op: string;
219
+ path: string;
220
+ value?: unknown;
221
+ }>>;
222
+ /** For "snapshot": full current state */
223
+ data?: Record<string, unknown>;
224
+ /** Error message if status is "error" */
225
+ error?: string;
226
+ }
227
+ /**
228
+ * Context passed to onUpdateFields hook.
229
+ */
230
+ interface UpdateFieldsContext2 {
231
+ /** Client ID */
26
232
  clientId: string;
27
- fields: Set<string> | "*";
233
+ /** Subscription ID */
234
+ subscriptionId: string;
235
+ /** Entity type */
236
+ entity: string;
237
+ /** Entity ID */
238
+ entityId: string;
239
+ /** New fields after update */
240
+ fields: string[] | "*";
241
+ /** Previous fields */
242
+ previousFields: string[] | "*";
28
243
  }
29
- /** Configuration */
30
- interface GraphStateManagerConfig {
31
- /** Called when an entity has no more subscribers */
32
- onEntityUnsubscribed?: (entity: string, id: string) => void;
244
+ /**
245
+ * Context passed to enhanceOperationMeta hook.
246
+ * Called for each operation when building handshake metadata.
247
+ */
248
+ interface EnhanceOperationMetaContext {
249
+ /** Operation path (e.g., 'user.create') */
250
+ path: string;
251
+ /** Operation type */
252
+ type: "query" | "mutation";
253
+ /** Current metadata (can be modified) */
254
+ meta: Record<string, unknown>;
255
+ /** Operation definition (MutationDef or QueryDef) */
256
+ definition: unknown;
33
257
  }
34
258
  /**
35
- * Manages server-side canonical state and syncs to clients.
259
+ * Server plugin interface.
260
+ *
261
+ * Plugins receive lifecycle hooks to extend server behavior.
262
+ * All hooks are optional - implement only what you need.
36
263
  *
37
264
  * @example
38
265
  * ```typescript
39
- * const manager = new GraphStateManager();
40
- *
41
- * // Add client
42
- * manager.addClient({
43
- * id: "client-1",
44
- * send: (msg) => ws.send(JSON.stringify(msg)),
45
- * });
46
- *
47
- * // Subscribe client to entity
48
- * manager.subscribe("client-1", "Post", "123", ["title", "content"]);
49
- *
50
- * // Emit updates (from resolvers)
51
- * manager.emit("Post", "123", { content: "Updated content" });
52
- * // → Automatically computes diff and sends to subscribed clients
266
+ * const loggingPlugin: ServerPlugin = {
267
+ * name: 'logging',
268
+ * onSubscribe: (ctx) => {
269
+ * console.log(`Client ${ctx.clientId} subscribed to ${ctx.operation}`);
270
+ * },
271
+ * beforeSend: (ctx) => {
272
+ * console.log(`Sending ${Object.keys(ctx.data).length} fields to ${ctx.clientId}`);
273
+ * return ctx.data; // Can modify data
274
+ * },
275
+ * };
53
276
  * ```
54
277
  */
55
- declare class GraphStateManager {
56
- /** Connected clients */
57
- private clients;
58
- /** Canonical state per entity (server truth) */
59
- private canonical;
60
- /** Canonical array state per entity (server truth for array outputs) */
61
- private canonicalArrays;
62
- /** Per-client state tracking */
63
- private clientStates;
64
- /** Per-client array state tracking */
65
- private clientArrayStates;
66
- /** Entity → subscribed client IDs */
67
- private entitySubscribers;
68
- /** Configuration */
69
- private config;
70
- constructor(config?: GraphStateManagerConfig);
278
+ interface ServerPlugin {
279
+ /** Plugin name (for debugging) */
280
+ name: string;
71
281
  /**
72
- * Add a client connection
282
+ * Called when a client connects.
283
+ * Can return false to reject the connection.
73
284
  */
74
- addClient(client: StateClient): void;
285
+ onConnect?: (ctx: ConnectContext) => void | boolean | Promise<void | boolean>;
75
286
  /**
76
- * Remove a client and cleanup all subscriptions
287
+ * Called when a client disconnects.
77
288
  */
78
- removeClient(clientId: string): void;
289
+ onDisconnect?: (ctx: DisconnectContext) => void | Promise<void>;
79
290
  /**
80
- * Subscribe a client to an entity
291
+ * Called when a client subscribes to an operation.
292
+ * Can modify the context or return false to reject.
81
293
  */
82
- subscribe(clientId: string, entity: string, id: string, fields?: string[] | "*"): void;
294
+ onSubscribe?: (ctx: SubscribeContext2) => void | boolean | Promise<void | boolean>;
83
295
  /**
84
- * Unsubscribe a client from an entity
296
+ * Called when a client unsubscribes.
85
297
  */
86
- unsubscribe(clientId: string, entity: string, id: string): void;
298
+ onUnsubscribe?: (ctx: UnsubscribeContext2) => void | Promise<void>;
87
299
  /**
88
- * Update subscription fields for a client
300
+ * Called before sending data to a client.
301
+ * Can modify the data to be sent.
302
+ *
303
+ * @returns Modified data, or undefined to use original
89
304
  */
90
- updateSubscription(clientId: string, entity: string, id: string, fields: string[] | "*"): void;
305
+ beforeSend?: (ctx: BeforeSendContext) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;
91
306
  /**
92
- * Emit data for an entity.
93
- * This is the core method called by resolvers.
94
- *
95
- * @param entity - Entity name
96
- * @param id - Entity ID
97
- * @param data - Full or partial entity data
98
- * @param options - Emit options
307
+ * Called after data is sent to a client.
99
308
  */
100
- emit(entity: string, id: string, data: Record<string, unknown>, options?: {
101
- replace?: boolean;
102
- }): void;
309
+ afterSend?: (ctx: AfterSendContext) => void | Promise<void>;
103
310
  /**
104
- * Emit a field-level update with a specific strategy.
105
- * Applies the update to canonical state and pushes to clients.
106
- *
107
- * @param entity - Entity name
108
- * @param id - Entity ID
109
- * @param field - Field name to update
110
- * @param update - Update with strategy (value/delta/patch)
311
+ * Called before a mutation is executed.
312
+ * Can modify the input or return false to reject.
111
313
  */
112
- emitField(entity: string, id: string, field: string, update: Update): void;
314
+ beforeMutation?: (ctx: BeforeMutationContext) => void | boolean | Promise<void | boolean>;
113
315
  /**
114
- * Emit multiple field updates in a batch.
115
- * More efficient than multiple emitField calls.
116
- *
117
- * @param entity - Entity name
118
- * @param id - Entity ID
119
- * @param updates - Array of field updates
316
+ * Called after a mutation is executed.
120
317
  */
121
- emitBatch(entity: string, id: string, updates: InternalFieldUpdate[]): void;
318
+ afterMutation?: (ctx: AfterMutationContext) => void | Promise<void>;
122
319
  /**
123
- * Process an EmitCommand from the Emit API.
124
- * Routes to appropriate emit method.
320
+ * Called when a client reconnects with subscription state.
321
+ * Plugin can return sync results for each subscription.
125
322
  *
126
- * @param entity - Entity name
127
- * @param id - Entity ID
128
- * @param command - Emit command from resolver
323
+ * @returns Array of sync results, or null to let other plugins handle
129
324
  */
130
- processCommand(entity: string, id: string, command: EmitCommand): void;
325
+ onReconnect?: (ctx: ReconnectContext2) => ReconnectHookResult2[] | null | Promise<ReconnectHookResult2[] | null>;
131
326
  /**
132
- * Emit array data (replace entire array).
327
+ * Called when a client updates subscribed fields for an entity.
328
+ */
329
+ onUpdateFields?: (ctx: UpdateFieldsContext2) => void | Promise<void>;
330
+ /**
331
+ * Called for each operation when building handshake metadata.
332
+ * Plugin can add fields to meta (e.g., optimistic config).
133
333
  *
134
- * @param entity - Entity name
135
- * @param id - Entity ID
136
- * @param items - Array items
334
+ * @example
335
+ * ```typescript
336
+ * enhanceOperationMeta: (ctx) => {
337
+ * if (ctx.type === 'mutation' && ctx.definition._optimistic) {
338
+ * ctx.meta.optimistic = convertToExecutable(ctx.definition._optimistic);
339
+ * }
340
+ * }
341
+ * ```
137
342
  */
138
- emitArray(entity: string, id: string, items: unknown[]): void;
343
+ enhanceOperationMeta?: (ctx: EnhanceOperationMetaContext) => void;
139
344
  /**
140
- * Apply an array operation to the canonical state.
345
+ * Called when broadcasting data to subscribers of an entity.
346
+ * Plugin updates canonical state and returns patch info.
347
+ * Handler is responsible for routing to subscribers.
141
348
  *
142
- * @param entity - Entity name
143
- * @param id - Entity ID
144
- * @param operation - Array operation to apply
349
+ * @returns BroadcastResult with version, patch, and data
350
+ */
351
+ onBroadcast?: (ctx: BroadcastContext) => {
352
+ version: number;
353
+ patch: unknown[] | null;
354
+ data: Record<string, unknown>;
355
+ } | boolean | void | Promise<{
356
+ version: number;
357
+ patch: unknown[] | null;
358
+ data: Record<string, unknown>;
359
+ } | boolean | void>;
360
+ }
361
+ /**
362
+ * Plugin manager handles plugin lifecycle and hook execution.
363
+ */
364
+ declare class PluginManager {
365
+ private plugins;
366
+ /**
367
+ * Register a plugin.
145
368
  */
146
- emitArrayOperation(entity: string, id: string, operation: ArrayOperation): void;
369
+ register(plugin: ServerPlugin): void;
147
370
  /**
148
- * Apply an array operation and return new array.
371
+ * Get all registered plugins.
149
372
  */
150
- private applyArrayOperation;
373
+ getPlugins(): readonly ServerPlugin[];
151
374
  /**
152
- * Push array update to a specific client.
153
- * Computes optimal diff strategy.
375
+ * Run onConnect hooks.
376
+ * Returns false if any plugin rejects the connection.
154
377
  */
155
- private pushArrayToClient;
378
+ runOnConnect(ctx: ConnectContext): Promise<boolean>;
156
379
  /**
157
- * Get current canonical array state
380
+ * Run onDisconnect hooks.
158
381
  */
159
- getArrayState(entity: string, id: string): unknown[] | undefined;
382
+ runOnDisconnect(ctx: DisconnectContext): Promise<void>;
160
383
  /**
161
- * Get current canonical state for an entity
384
+ * Run onSubscribe hooks.
385
+ * Returns false if any plugin rejects the subscription.
162
386
  */
163
- getState(entity: string, id: string): Record<string, unknown> | undefined;
387
+ runOnSubscribe(ctx: SubscribeContext2): Promise<boolean>;
164
388
  /**
165
- * Check if entity has any subscribers
389
+ * Run onUnsubscribe hooks.
166
390
  */
167
- hasSubscribers(entity: string, id: string): boolean;
391
+ runOnUnsubscribe(ctx: UnsubscribeContext2): Promise<void>;
168
392
  /**
169
- * Push update to a specific client
393
+ * Run beforeSend hooks.
394
+ * Each plugin can modify the data.
170
395
  */
171
- private pushToClient;
396
+ runBeforeSend(ctx: BeforeSendContext): Promise<Record<string, unknown>>;
172
397
  /**
173
- * Push a single field update to a client.
174
- * Computes optimal transfer strategy.
398
+ * Run afterSend hooks.
175
399
  */
176
- private pushFieldToClient;
400
+ runAfterSend(ctx: AfterSendContext): Promise<void>;
177
401
  /**
178
- * Push multiple field updates to a client.
179
- * Computes optimal transfer strategy for each field.
402
+ * Run beforeMutation hooks.
403
+ * Returns false if any plugin rejects the mutation.
180
404
  */
181
- private pushFieldsToClient;
405
+ runBeforeMutation(ctx: BeforeMutationContext): Promise<boolean>;
182
406
  /**
183
- * Send initial data to a newly subscribed client
407
+ * Run afterMutation hooks.
184
408
  */
185
- private sendInitialData;
409
+ runAfterMutation(ctx: AfterMutationContext): Promise<void>;
186
410
  /**
187
- * Cleanup entity when no subscribers remain
411
+ * Run onReconnect hooks.
412
+ * Returns the first non-null result from a plugin.
188
413
  */
189
- private cleanupEntity;
190
- private makeKey;
414
+ runOnReconnect(ctx: ReconnectContext2): Promise<ReconnectHookResult2[] | null>;
191
415
  /**
192
- * Get statistics
416
+ * Run onUpdateFields hooks.
193
417
  */
194
- getStats(): {
195
- clients: number;
196
- entities: number;
197
- totalSubscriptions: number;
198
- };
418
+ runOnUpdateFields(ctx: UpdateFieldsContext2): Promise<void>;
199
419
  /**
200
- * Clear all state (for testing)
420
+ * Run enhanceOperationMeta hooks.
421
+ * Each plugin can add fields to the operation metadata.
201
422
  */
202
- clear(): void;
423
+ runEnhanceOperationMeta(ctx: EnhanceOperationMetaContext): void;
424
+ /**
425
+ * Run onBroadcast hooks.
426
+ * Returns BroadcastResult if a plugin handled it, null otherwise.
427
+ */
428
+ runOnBroadcast(ctx: BroadcastContext): Promise<{
429
+ version: number;
430
+ patch: unknown[] | null;
431
+ data: Record<string, unknown>;
432
+ } | null>;
203
433
  }
204
434
  /**
205
- * Create a GraphStateManager instance
435
+ * Create a new plugin manager.
436
+ */
437
+ declare function createPluginManager(): PluginManager;
438
+ import { FieldType } from "@sylphx/lens-core";
439
+ /**
440
+ * Nested selection object with optional input.
441
+ * Used for relations that need their own params.
442
+ */
443
+ interface NestedSelection {
444
+ /** Input/params for this nested query */
445
+ input?: Record<string, unknown>;
446
+ /** Field selection for this nested query */
447
+ select?: SelectionObject;
448
+ }
449
+ /**
450
+ * Selection object for field selection.
451
+ * Supports:
452
+ * - `true` - Select this field
453
+ * - `{ select: {...} }` - Nested selection only
454
+ * - `{ input: {...}, select?: {...} }` - Nested with input params
206
455
  */
207
- declare function createGraphStateManager(config?: GraphStateManagerConfig): GraphStateManager;
208
- /** Selection object type for nested field selection */
209
456
  interface SelectionObject {
210
457
  [key: string]: boolean | SelectionObject | {
211
458
  select: SelectionObject;
212
- };
459
+ } | NestedSelection;
213
460
  }
214
461
  /** Entity map type */
215
462
  type EntitiesMap = Record<string, EntityDef<string, any>>;
@@ -220,13 +467,13 @@ type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
220
467
  /** Operation metadata for handshake */
221
468
  interface OperationMeta {
222
469
  type: "query" | "mutation" | "subscription";
223
- optimistic?: unknown;
470
+ optimistic?: OptimisticDSL;
224
471
  }
225
472
  /** Nested operations structure for handshake */
226
473
  type OperationsMap = {
227
474
  [key: string]: OperationMeta | OperationsMap;
228
475
  };
229
- /** Logger interface for server */
476
+ /** Logger interface */
230
477
  interface LensLogger {
231
478
  info?: (message: string, ...args: unknown[]) => void;
232
479
  warn?: (message: string, ...args: unknown[]) => void;
@@ -234,75 +481,51 @@ interface LensLogger {
234
481
  }
235
482
  /** Server configuration */
236
483
  interface LensServerConfig<
237
- TContext extends ContextValue = ContextValue,
484
+ TContext extends ContextValue2 = ContextValue2,
238
485
  TRouter extends RouterDef = RouterDef
239
486
  > {
240
487
  /** Entity definitions */
241
488
  entities?: EntitiesMap | undefined;
242
- /** Router definition (namespaced operations) - context type is inferred */
489
+ /** Router definition (namespaced operations) */
243
490
  router?: TRouter | undefined;
244
- /** Query definitions (flat, legacy) */
491
+ /** Query definitions (flat) */
245
492
  queries?: QueriesMap | undefined;
246
- /** Mutation definitions (flat, legacy) */
493
+ /** Mutation definitions (flat) */
247
494
  mutations?: MutationsMap | undefined;
248
- /** Field resolvers array (use lens() factory to create) */
495
+ /** Field resolvers array */
249
496
  resolvers?: Resolvers | undefined;
250
497
  /** Logger for server messages (default: silent) */
251
498
  logger?: LensLogger | undefined;
252
- /** Context factory - must return the context type expected by the router */
499
+ /** Context factory */
253
500
  context?: ((req?: unknown) => TContext | Promise<TContext>) | undefined;
254
501
  /** Server version */
255
502
  version?: string | undefined;
503
+ /**
504
+ * Server-level plugins for subscription lifecycle and state management.
505
+ * Plugins are processed at the server level, not adapter level.
506
+ */
507
+ plugins?: ServerPlugin[] | undefined;
256
508
  }
257
509
  /** Server metadata for transport handshake */
258
510
  interface ServerMetadata {
259
- /** Server version */
260
511
  version: string;
261
- /** Operations metadata map */
262
512
  operations: OperationsMap;
263
513
  }
264
- /** Operation for in-process transport */
514
+ /** Operation for execution */
265
515
  interface LensOperation {
266
- /** Operation path (e.g., 'user.get', 'session.create') */
267
516
  path: string;
268
- /** Operation input */
269
517
  input?: unknown;
270
518
  }
271
519
  /** Result from operation execution */
272
520
  interface LensResult<T = unknown> {
273
- /** Success data */
274
521
  data?: T;
275
- /** Error if operation failed */
276
522
  error?: Error;
277
523
  }
278
- /** Lens server interface */
279
- interface LensServer {
280
- /** Get server metadata for transport handshake */
281
- getMetadata(): ServerMetadata;
282
- /** Execute operation - auto-detects query vs mutation from registered operations */
283
- execute(op: LensOperation): Promise<LensResult>;
284
- /** Execute a query (one-time) */
285
- executeQuery<
286
- TInput,
287
- TOutput
288
- >(name: string, input?: TInput): Promise<TOutput>;
289
- /** Execute a mutation */
290
- executeMutation<
291
- TInput,
292
- TOutput
293
- >(name: string, input: TInput): Promise<TOutput>;
294
- /** Handle WebSocket connection */
295
- handleWebSocket(ws: WebSocketLike): void;
296
- /** Handle HTTP request */
297
- handleRequest(req: Request): Promise<Response>;
298
- /** Get GraphStateManager for external access */
299
- getStateManager(): GraphStateManager;
300
- /** Start server */
301
- listen(port: number): Promise<void>;
302
- /** Close server */
303
- close(): Promise<void>;
304
- }
305
- /** WebSocket interface */
524
+ /**
525
+ * Client send function type for subscription updates.
526
+ */
527
+ type ClientSendFn = (message: unknown) => void;
528
+ /** WebSocket interface for adapters */
306
529
  interface WebSocketLike {
307
530
  send(data: string): void;
308
531
  close(): void;
@@ -313,86 +536,128 @@ interface WebSocketLike {
313
536
  onerror?: ((error: unknown) => void) | null;
314
537
  }
315
538
  /**
316
- * Infer input type from a query/mutation definition
317
- */
318
- type InferInput<T> = T extends QueryDef<infer I, unknown> ? I extends void ? void : I : T extends MutationDef<infer I, unknown> ? I : never;
319
- /**
320
- * Infer output type from a query/mutation definition
321
- */
322
- type InferOutput<T> = T extends QueryDef<unknown, infer O> ? O : T extends MutationDef<unknown, infer O> ? O : never;
323
- /**
324
- * API type for client inference
325
- * Export this type for client-side type safety
539
+ * Lens server interface
326
540
  *
327
- * @example
328
- * ```typescript
329
- * // Server
330
- * const server = createLensServer({ queries, mutations });
331
- * type Api = InferApi<typeof server>;
541
+ * Core methods:
542
+ * - getMetadata() - Server metadata for transport handshake
543
+ * - execute() - Execute any operation
332
544
  *
333
- * // Client (only imports TYPE)
334
- * import type { Api } from './server';
335
- * const client = createClient<Api>({ links: [...] });
336
- * ```
545
+ * Subscription support (used by adapters):
546
+ * - addClient() / removeClient() - Client connection management
547
+ * - subscribe() / unsubscribe() - Subscription lifecycle
548
+ * - send() - Send data to client (runs through plugin hooks)
549
+ * - broadcast() - Broadcast to all entity subscribers
550
+ * - handleReconnect() - Handle client reconnection
551
+ *
552
+ * The server handles all business logic including state management (via plugins).
553
+ * Handlers are pure protocol translators that call these methods.
337
554
  */
555
+ interface LensServer {
556
+ /** Get server metadata for transport handshake */
557
+ getMetadata(): ServerMetadata;
558
+ /** Execute operation - auto-detects query vs mutation */
559
+ execute(op: LensOperation): Promise<LensResult>;
560
+ /**
561
+ * Register a client connection.
562
+ * Call when a client connects via WebSocket/SSE.
563
+ */
564
+ addClient(clientId: string, send: ClientSendFn): Promise<boolean>;
565
+ /**
566
+ * Remove a client connection.
567
+ * Call when a client disconnects.
568
+ */
569
+ removeClient(clientId: string, subscriptionCount: number): void;
570
+ /**
571
+ * Subscribe a client to an entity.
572
+ * Runs plugin hooks and sets up state tracking (if clientState is enabled).
573
+ */
574
+ subscribe(ctx: import("../plugin/types.js").SubscribeContext): Promise<boolean>;
575
+ /**
576
+ * Unsubscribe a client from an entity.
577
+ * Runs plugin hooks and cleans up state tracking.
578
+ */
579
+ unsubscribe(ctx: import("../plugin/types.js").UnsubscribeContext): void;
580
+ /**
581
+ * Send data to a client for a specific subscription.
582
+ * Runs through plugin hooks (beforeSend/afterSend) for optimization.
583
+ */
584
+ send(clientId: string, subscriptionId: string, entity: string, entityId: string, data: Record<string, unknown>, isInitial: boolean): Promise<void>;
585
+ /**
586
+ * Broadcast data to all subscribers of an entity.
587
+ * Runs through plugin hooks for each subscriber.
588
+ */
589
+ broadcast(entity: string, entityId: string, data: Record<string, unknown>): Promise<void>;
590
+ /**
591
+ * Handle a reconnection request from a client.
592
+ * Uses plugin hooks (onReconnect) for reconnection logic.
593
+ */
594
+ handleReconnect(ctx: import("../plugin/types.js").ReconnectContext): Promise<import("../plugin/types.js").ReconnectHookResult[] | null>;
595
+ /**
596
+ * Update subscribed fields for a client's subscription.
597
+ * Runs plugin hooks (onUpdateFields) to sync state.
598
+ */
599
+ updateFields(ctx: import("../plugin/types.js").UpdateFieldsContext): Promise<void>;
600
+ /**
601
+ * Get the plugin manager for direct hook access.
602
+ */
603
+ getPluginManager(): PluginManager;
604
+ }
605
+ type InferInput<T> = T extends QueryDef<infer I, any> ? I : T extends MutationDef<infer I, any> ? I : never;
606
+ type InferOutput<T> = T extends QueryDef<any, infer O> ? O : T extends MutationDef<any, infer O> ? O : T extends FieldType<infer F> ? F : never;
338
607
  type InferApi<T> = T extends {
339
608
  _types: infer Types;
340
609
  } ? Types : never;
341
- /**
342
- * Config helper type that infers context from router
343
- */
344
610
  type ServerConfigWithInferredContext<
345
611
  TRouter extends RouterDef,
346
612
  Q extends QueriesMap = QueriesMap,
347
613
  M extends MutationsMap = MutationsMap
348
614
  > = {
349
- entities?: EntitiesMap;
350
615
  router: TRouter;
616
+ entities?: EntitiesMap;
351
617
  queries?: Q;
352
618
  mutations?: M;
353
- /** Field resolvers array */
354
619
  resolvers?: Resolvers;
355
- /** Context factory - type is inferred from router's procedures */
356
- context?: (req?: unknown) => InferRouterContext<TRouter> | Promise<InferRouterContext<TRouter>>;
620
+ logger?: LensLogger;
621
+ context?: () => InferRouterContext<TRouter> | Promise<InferRouterContext<TRouter>>;
357
622
  version?: string;
623
+ /** Server-level plugins (clientState, etc.) */
624
+ plugins?: ServerPlugin[];
358
625
  };
359
- /**
360
- * Config without router (legacy flat queries/mutations)
361
- */
362
626
  type ServerConfigLegacy<
363
- TContext extends ContextValue = ContextValue,
627
+ TContext extends ContextValue2,
364
628
  Q extends QueriesMap = QueriesMap,
365
629
  M extends MutationsMap = MutationsMap
366
630
  > = {
631
+ router?: RouterDef | undefined;
367
632
  entities?: EntitiesMap;
368
- router?: undefined;
369
633
  queries?: Q;
370
634
  mutations?: M;
371
- /** Field resolvers array */
372
635
  resolvers?: Resolvers;
373
- context?: (req?: unknown) => TContext | Promise<TContext>;
636
+ logger?: LensLogger;
637
+ context?: () => TContext | Promise<TContext>;
374
638
  version?: string;
639
+ /** Server-level plugins (clientState, etc.) */
640
+ plugins?: ServerPlugin[];
375
641
  };
376
642
  /**
377
- * Create Lens server with Operations API + Optimization Layer
378
- *
379
- * When using a router with typed context (from initLens), the context
380
- * function's return type is automatically enforced to match.
643
+ * Create Lens server with optional plugin support.
381
644
  *
382
645
  * @example
383
646
  * ```typescript
384
- * // Context type is inferred from router's procedures
385
- * const server = createServer({
386
- * router: appRouter, // RouterDef with MyContext
387
- * context: () => ({
388
- * db: prisma,
389
- * user: null,
390
- * }), // Must match MyContext!
391
- * })
647
+ * // Stateless mode (default)
648
+ * const app = createApp({ router });
649
+ * createWSHandler(app); // Sends full data on each update
650
+ *
651
+ * // Stateful mode (with clientState)
652
+ * const app = createApp({
653
+ * router,
654
+ * plugins: [clientState()], // Enables per-client state tracking
655
+ * });
656
+ * createWSHandler(app); // Sends minimal diffs
392
657
  * ```
393
658
  */
394
- declare function createServer<
395
- TRouter extends RouterDef,
659
+ declare function createApp<
660
+ TRouter extends RouterDef2,
396
661
  Q extends QueriesMap = QueriesMap,
397
662
  M extends MutationsMap = MutationsMap
398
663
  >(config: ServerConfigWithInferredContext<TRouter, Q, M>): LensServer & {
@@ -400,11 +665,11 @@ declare function createServer<
400
665
  router: TRouter;
401
666
  queries: Q;
402
667
  mutations: M;
403
- context: InferRouterContext<TRouter>;
668
+ context: InferRouterContext2<TRouter>;
404
669
  };
405
670
  };
406
- declare function createServer<
407
- TContext extends ContextValue = ContextValue,
671
+ declare function createApp<
672
+ TContext extends ContextValue3 = ContextValue3,
408
673
  Q extends QueriesMap = QueriesMap,
409
674
  M extends MutationsMap = MutationsMap
410
675
  >(config: ServerConfigLegacy<TContext, Q, M>): LensServer & {
@@ -414,74 +679,809 @@ declare function createServer<
414
679
  context: TContext;
415
680
  };
416
681
  };
682
+ /**
683
+ * @sylphx/lens-server - SSE Handler
684
+ *
685
+ * Pure transport handler for Server-Sent Events.
686
+ * No state management - just handles SSE connection lifecycle and message sending.
687
+ */
417
688
  /** SSE handler configuration */
418
689
  interface SSEHandlerConfig {
419
- /** GraphStateManager instance (required) */
420
- stateManager: GraphStateManager;
421
690
  /** Heartbeat interval in ms (default: 30000) */
422
691
  heartbeatInterval?: number;
692
+ /** Called when a client connects */
693
+ onConnect?: (client: SSEClient) => void;
694
+ /** Called when a client disconnects */
695
+ onDisconnect?: (clientId: string) => void;
423
696
  }
424
- /** SSE client info */
425
- interface SSEClientInfo {
697
+ /** SSE client handle for sending messages */
698
+ interface SSEClient {
699
+ /** Unique client ID */
426
700
  id: string;
427
- connectedAt: number;
701
+ /** Send a message to this client */
702
+ send: (message: unknown) => void;
703
+ /** Send a named event to this client */
704
+ sendEvent: (event: string, data: unknown) => void;
705
+ /** Close this client's connection */
706
+ close: () => void;
428
707
  }
429
708
  /**
430
- * SSE transport adapter for GraphStateManager.
709
+ * Pure SSE transport handler.
431
710
  *
432
- * This is a thin adapter that:
433
- * - Creates SSE connections
434
- * - Registers clients with GraphStateManager
435
- * - Forwards updates to SSE streams
711
+ * This handler ONLY manages:
712
+ * - SSE connection lifecycle
713
+ * - Message sending to clients
714
+ * - Heartbeat keepalive
436
715
  *
437
- * All state/subscription logic is handled by GraphStateManager.
716
+ * It does NOT know about:
717
+ * - State management
718
+ * - Subscriptions
719
+ * - Plugins
438
720
  *
439
721
  * @example
440
722
  * ```typescript
441
- * const stateManager = new GraphStateManager();
442
- * const sse = new SSEHandler({ stateManager });
723
+ * const sse = new SSEHandler({
724
+ * onConnect: (client) => {
725
+ * console.log('Client connected:', client.id);
726
+ * // Register with your state management here
727
+ * },
728
+ * onDisconnect: (clientId) => {
729
+ * console.log('Client disconnected:', clientId);
730
+ * // Cleanup your state management here
731
+ * },
732
+ * });
443
733
  *
444
734
  * // Handle SSE connection
445
735
  * app.get('/events', (req) => sse.handleConnection(req));
446
736
  *
447
- * // Subscribe via separate endpoint or message
448
- * stateManager.subscribe(clientId, "Post", "123", "*");
737
+ * // Send message to specific client
738
+ * sse.send(clientId, { type: 'update', data: {...} });
449
739
  * ```
450
740
  */
451
741
  declare class SSEHandler {
452
- private stateManager;
453
742
  private heartbeatInterval;
743
+ private onConnectCallback;
744
+ private onDisconnectCallback;
454
745
  private clients;
455
746
  private clientCounter;
456
- constructor(config: SSEHandlerConfig);
747
+ constructor(config?: SSEHandlerConfig);
457
748
  /**
458
- * Handle new SSE connection
459
- * Returns a Response with SSE stream
749
+ * Handle new SSE connection.
750
+ * Returns a Response with SSE stream.
460
751
  */
461
752
  handleConnection(_req?: Request): Response;
462
753
  /**
463
- * Remove client and cleanup
754
+ * Send a message to a specific client.
755
+ */
756
+ send(clientId: string, message: unknown): boolean;
757
+ /**
758
+ * Send a named event to a specific client.
759
+ */
760
+ sendEvent(clientId: string, event: string, data: unknown): boolean;
761
+ /**
762
+ * Broadcast a message to all connected clients.
763
+ */
764
+ broadcast(message: unknown): void;
765
+ /**
766
+ * Remove client and cleanup.
464
767
  */
465
768
  private removeClient;
466
769
  /**
467
- * Close specific client connection
770
+ * Close specific client connection.
468
771
  */
469
772
  closeClient(clientId: string): void;
470
773
  /**
471
- * Get connected client count
774
+ * Get connected client count.
472
775
  */
473
776
  getClientCount(): number;
474
777
  /**
475
- * Get connected client IDs
778
+ * Get connected client IDs.
476
779
  */
477
780
  getClientIds(): string[];
478
781
  /**
479
- * Close all connections
782
+ * Check if a client is connected.
783
+ */
784
+ hasClient(clientId: string): boolean;
785
+ /**
786
+ * Close all connections.
480
787
  */
481
788
  closeAll(): void;
482
789
  }
483
790
  /**
484
- * Create SSE handler (transport adapter)
791
+ * Create SSE handler (pure transport).
792
+ */
793
+ declare function createSSEHandler(config?: SSEHandlerConfig): SSEHandler;
794
+ interface HTTPHandlerOptions {
795
+ /**
796
+ * Path prefix for Lens endpoints.
797
+ * Default: "" (no prefix)
798
+ *
799
+ * @example
800
+ * ```typescript
801
+ * // All endpoints under /api
802
+ * createHTTPHandler(app, { pathPrefix: '/api' })
803
+ * // Metadata: GET /api/__lens/metadata
804
+ * // Operations: POST /api
805
+ * ```
806
+ */
807
+ pathPrefix?: string;
808
+ /**
809
+ * Custom CORS headers.
810
+ * Default: Allow all origins
811
+ */
812
+ cors?: {
813
+ origin?: string | string[];
814
+ methods?: string[];
815
+ headers?: string[];
816
+ };
817
+ }
818
+ interface HTTPHandler {
819
+ /**
820
+ * Handle HTTP request.
821
+ * Compatible with fetch API (Bun, Cloudflare Workers, Vercel).
822
+ */
823
+ (request: Request): Promise<Response>;
824
+ /**
825
+ * Alternative method-style call.
826
+ */
827
+ handle(request: Request): Promise<Response>;
828
+ }
829
+ /**
830
+ * Create an HTTP handler from a Lens app.
831
+ *
832
+ * @example
833
+ * ```typescript
834
+ * import { createApp, createHTTPHandler } from '@sylphx/lens-server'
835
+ *
836
+ * const app = createApp({ router })
837
+ * const handler = createHTTPHandler(app)
838
+ *
839
+ * // Bun
840
+ * Bun.serve({ port: 3000, fetch: handler })
841
+ *
842
+ * // Vercel
843
+ * handler
844
+ *
845
+ * // Cloudflare Workers
846
+ * { fetch: handler }
847
+ * ```
848
+ */
849
+ declare function createHTTPHandler(server: LensServer, options?: HTTPHandlerOptions): HTTPHandler;
850
+ interface HandlerOptions extends HTTPHandlerOptions {
851
+ /**
852
+ * SSE endpoint path.
853
+ * Default: "/__lens/sse"
854
+ */
855
+ ssePath?: string;
856
+ /**
857
+ * Heartbeat interval for SSE connections in ms.
858
+ * Default: 30000
859
+ */
860
+ heartbeatInterval?: number;
861
+ }
862
+ interface Handler {
863
+ /**
864
+ * Handle HTTP/SSE request.
865
+ * Compatible with fetch API (Bun, Cloudflare Workers, Vercel).
866
+ */
867
+ (request: Request): Promise<Response>;
868
+ /**
869
+ * Alternative method-style call.
870
+ */
871
+ handle(request: Request): Promise<Response>;
872
+ /**
873
+ * Access the SSE handler for manual operations.
874
+ */
875
+ sse: SSEHandler;
876
+ }
877
+ /**
878
+ * Create a unified HTTP + SSE handler from a Lens app.
879
+ *
880
+ * Automatically routes:
881
+ * - GET {ssePath} → SSE connection
882
+ * - Other requests → HTTP handler
883
+ *
884
+ * @example
885
+ * ```typescript
886
+ * import { createApp, createHandler } from '@sylphx/lens-server'
887
+ *
888
+ * const app = createApp({ router })
889
+ * const handler = createHandler(app)
890
+ *
891
+ * // Bun
892
+ * Bun.serve({ port: 3000, fetch: handler })
893
+ *
894
+ * // SSE endpoint: GET /__lens/sse
895
+ * // HTTP endpoints: POST /, GET /__lens/metadata
896
+ * ```
897
+ */
898
+ declare function createHandler(server: LensServer, options?: HandlerOptions): Handler;
899
+ /**
900
+ * Create a proxy object that provides typed access to server procedures.
901
+ *
902
+ * This proxy allows calling server procedures directly without going through
903
+ * HTTP. Useful for:
904
+ * - Server-side rendering (SSR)
905
+ * - Server Components
906
+ * - Testing
907
+ * - Same-process communication
908
+ *
909
+ * @example
910
+ * ```typescript
911
+ * const serverClient = createServerClientProxy(server);
912
+ *
913
+ * // Call procedures directly (typed!)
914
+ * const users = await serverClient.user.list();
915
+ * const user = await serverClient.user.get({ id: '123' });
916
+ * ```
917
+ */
918
+ declare function createServerClientProxy(server: LensServer): unknown;
919
+ /**
920
+ * Handle a query request using standard Web Request/Response API.
921
+ *
922
+ * Expects input in URL search params as JSON string.
923
+ *
924
+ * @example
925
+ * ```typescript
926
+ * // GET /api/lens/user.get?input={"id":"123"}
927
+ * const response = await handleWebQuery(server, 'user.get', url);
928
+ * ```
929
+ */
930
+ declare function handleWebQuery(server: LensServer, path: string, url: URL): Promise<Response>;
931
+ /**
932
+ * Handle a mutation request using standard Web Request/Response API.
933
+ *
934
+ * Expects input in request body as JSON.
935
+ *
936
+ * @example
937
+ * ```typescript
938
+ * // POST /api/lens/user.create with body { "input": { "name": "John" } }
939
+ * const response = await handleWebMutation(server, 'user.create', request);
940
+ * ```
941
+ */
942
+ declare function handleWebMutation(server: LensServer, path: string, request: Request): Promise<Response>;
943
+ /**
944
+ * Handle an SSE subscription request using standard Web Request/Response API.
945
+ *
946
+ * Creates a ReadableStream that emits SSE events from the subscription.
947
+ *
948
+ * @example
949
+ * ```typescript
950
+ * // GET /api/lens/events.stream with Accept: text/event-stream
951
+ * const response = handleWebSSE(server, 'events.stream', url, request.signal);
952
+ * ```
953
+ */
954
+ declare function handleWebSSE(server: LensServer, path: string, url: URL, signal?: AbortSignal): Response;
955
+ /**
956
+ * Options for creating a framework handler.
957
+ */
958
+ interface FrameworkHandlerOptions {
959
+ /** Base path to strip from request URLs */
960
+ basePath?: string;
961
+ }
962
+ /**
963
+ * Create a complete request handler for Web standard Request/Response.
964
+ *
965
+ * Handles:
966
+ * - GET requests → Query execution
967
+ * - POST requests → Mutation execution
968
+ * - SSE requests (Accept: text/event-stream) → Subscriptions
969
+ *
970
+ * @example
971
+ * ```typescript
972
+ * const handler = createFrameworkHandler(server, { basePath: '/api/lens' });
973
+ *
974
+ * // In Next.js App Router:
975
+ * const GET = handler;
976
+ * const POST = handler;
977
+ *
978
+ * // In Fresh:
979
+ * const handler = { GET: lensHandler, POST: lensHandler };
980
+ * ```
981
+ */
982
+ declare function createFrameworkHandler(server: LensServer, options?: FrameworkHandlerOptions): (request: Request) => Promise<Response>;
983
+ interface WSHandlerOptions {
984
+ /**
985
+ * Logger for debugging.
986
+ */
987
+ logger?: {
988
+ info?: (message: string, ...args: unknown[]) => void;
989
+ warn?: (message: string, ...args: unknown[]) => void;
990
+ error?: (message: string, ...args: unknown[]) => void;
991
+ };
992
+ }
993
+ /**
994
+ * WebSocket adapter for Bun's websocket handler.
995
+ */
996
+ interface WSHandler {
997
+ /**
998
+ * Handle a new WebSocket connection.
999
+ * Call this when a WebSocket connection is established.
1000
+ */
1001
+ handleConnection(ws: WebSocketLike): void;
1002
+ /**
1003
+ * Bun-compatible websocket handler object.
1004
+ * Use directly with Bun.serve({ websocket: wsHandler.handler })
1005
+ */
1006
+ handler: {
1007
+ message(ws: unknown, message: string | Buffer): void;
1008
+ close(ws: unknown): void;
1009
+ open?(ws: unknown): void;
1010
+ };
1011
+ /**
1012
+ * Close all connections and cleanup.
1013
+ */
1014
+ close(): Promise<void>;
1015
+ }
1016
+ /**
1017
+ * Create a WebSocket handler from a Lens app.
1018
+ *
1019
+ * The handler is a pure protocol translator - all business logic is in the server.
1020
+ * State management is controlled by server plugins (e.g., clientState).
1021
+ *
1022
+ * @example
1023
+ * ```typescript
1024
+ * import { createApp, createWSHandler, clientState } from '@sylphx/lens-server'
1025
+ *
1026
+ * // Stateless mode (default) - sends full data
1027
+ * const app = createApp({ router });
1028
+ * const wsHandler = createWSHandler(app);
1029
+ *
1030
+ * // Stateful mode - sends minimal diffs
1031
+ * const appWithState = createApp({
1032
+ * router,
1033
+ * plugins: [clientState()],
1034
+ * });
1035
+ * const wsHandlerWithState = createWSHandler(appWithState);
1036
+ *
1037
+ * // Bun
1038
+ * Bun.serve({
1039
+ * port: 3000,
1040
+ * fetch: httpHandler,
1041
+ * websocket: wsHandler.handler,
1042
+ * })
1043
+ * ```
1044
+ */
1045
+ declare function createWSHandler(server: LensServer, options?: WSHandlerOptions): WSHandler;
1046
+ import { PatchOperation as PatchOperation2 } from "@sylphx/lens-core";
1047
+ import { PatchOperation } from "@sylphx/lens-core";
1048
+ /**
1049
+ * Entity state stored in the operation log.
1050
+ */
1051
+ interface StoredEntityState {
1052
+ /** Canonical state data */
1053
+ data: Record<string, unknown>;
1054
+ /** Current version */
1055
+ version: number;
1056
+ /** Timestamp of last update */
1057
+ updatedAt: number;
1058
+ }
1059
+ /**
1060
+ * Operation log entry stored in storage.
1061
+ */
1062
+ interface StoredPatchEntry {
1063
+ /** Version this patch creates */
1064
+ version: number;
1065
+ /** Patch operations */
1066
+ patch: PatchOperation[];
1067
+ /** Timestamp when patch was created */
1068
+ timestamp: number;
1069
+ }
1070
+ /**
1071
+ * Result from emit operation.
1072
+ */
1073
+ interface EmitResult {
1074
+ /** New version after emit */
1075
+ version: number;
1076
+ /** Computed patch (null if first emit) */
1077
+ patch: PatchOperation[] | null;
1078
+ /** Whether state actually changed */
1079
+ changed: boolean;
1080
+ }
1081
+ /**
1082
+ * Storage adapter interface for opLog.
1083
+ *
1084
+ * Implementations:
1085
+ * - `memoryStorage()` - In-memory (default, for long-running servers)
1086
+ * - `redisStorage()` - Redis/Upstash (for serverless)
1087
+ * - `kvStorage()` - Cloudflare KV, Vercel KV
1088
+ *
1089
+ * All methods are async to support external storage.
1090
+ *
1091
+ * @example
1092
+ * ```typescript
1093
+ * // Default (in-memory)
1094
+ * const app = createApp({
1095
+ * router,
1096
+ * plugins: [opLog()],
1097
+ * });
1098
+ *
1099
+ * // With Redis for serverless
1100
+ * const app = createApp({
1101
+ * router,
1102
+ * plugins: [opLog({
1103
+ * storage: redisStorage({ url: process.env.REDIS_URL }),
1104
+ * })],
1105
+ * });
1106
+ * ```
1107
+ */
1108
+ interface OpLogStorage {
1109
+ /**
1110
+ * Emit new state for an entity.
1111
+ * This is an atomic operation that:
1112
+ * 1. Computes patch from previous state (if exists)
1113
+ * 2. Stores new state with incremented version
1114
+ * 3. Appends patch to operation log
1115
+ *
1116
+ * @param entity - Entity type name
1117
+ * @param entityId - Entity ID
1118
+ * @param data - New state data
1119
+ * @returns Emit result with version, patch, and changed flag
1120
+ */
1121
+ emit(entity: string, entityId: string, data: Record<string, unknown>): Promise<EmitResult>;
1122
+ /**
1123
+ * Get current canonical state for an entity.
1124
+ *
1125
+ * @param entity - Entity type name
1126
+ * @param entityId - Entity ID
1127
+ * @returns State data or null if not found
1128
+ */
1129
+ getState(entity: string, entityId: string): Promise<Record<string, unknown> | null>;
1130
+ /**
1131
+ * Get current version for an entity.
1132
+ * Returns 0 if entity doesn't exist.
1133
+ *
1134
+ * @param entity - Entity type name
1135
+ * @param entityId - Entity ID
1136
+ * @returns Current version (0 if not found)
1137
+ */
1138
+ getVersion(entity: string, entityId: string): Promise<number>;
1139
+ /**
1140
+ * Get the latest patch for an entity.
1141
+ * Returns null if no patches available.
1142
+ *
1143
+ * @param entity - Entity type name
1144
+ * @param entityId - Entity ID
1145
+ * @returns Latest patch or null
1146
+ */
1147
+ getLatestPatch(entity: string, entityId: string): Promise<PatchOperation[] | null>;
1148
+ /**
1149
+ * Get all patches since a given version.
1150
+ * Used for reconnection to bring client up to date.
1151
+ *
1152
+ * @param entity - Entity type name
1153
+ * @param entityId - Entity ID
1154
+ * @param sinceVersion - Client's current version
1155
+ * @returns Array of patches (one per version), or null if too old
1156
+ */
1157
+ getPatchesSince(entity: string, entityId: string, sinceVersion: number): Promise<PatchOperation[][] | null>;
1158
+ /**
1159
+ * Check if entity exists in storage.
1160
+ *
1161
+ * @param entity - Entity type name
1162
+ * @param entityId - Entity ID
1163
+ * @returns True if entity exists
1164
+ */
1165
+ has(entity: string, entityId: string): Promise<boolean>;
1166
+ /**
1167
+ * Delete an entity from storage.
1168
+ * Removes state, version, and all patches.
1169
+ *
1170
+ * @param entity - Entity type name
1171
+ * @param entityId - Entity ID
1172
+ */
1173
+ delete(entity: string, entityId: string): Promise<void>;
1174
+ /**
1175
+ * Clear all data from storage.
1176
+ * Used for testing.
1177
+ */
1178
+ clear(): Promise<void>;
1179
+ /**
1180
+ * Dispose storage resources.
1181
+ * Called when shutting down.
1182
+ */
1183
+ dispose?(): Promise<void>;
1184
+ }
1185
+ /**
1186
+ * Configuration for operation log storage.
1187
+ */
1188
+ interface OpLogStorageConfig {
1189
+ /**
1190
+ * Maximum number of patches to keep per entity.
1191
+ * Older patches are evicted when limit is reached.
1192
+ * @default 1000
1193
+ */
1194
+ maxPatchesPerEntity?: number;
1195
+ /**
1196
+ * Maximum age of patches in milliseconds.
1197
+ * Patches older than this are evicted.
1198
+ * @default 300000 (5 minutes)
1199
+ */
1200
+ maxPatchAge?: number;
1201
+ /**
1202
+ * Cleanup interval in milliseconds.
1203
+ * Set to 0 to disable automatic cleanup.
1204
+ * @default 60000 (1 minute)
1205
+ */
1206
+ cleanupInterval?: number;
1207
+ /**
1208
+ * Maximum retries for emit operations on version conflict.
1209
+ * Only applies to external storage (Redis, Upstash, Vercel KV).
1210
+ * Set to 0 to disable retries (fail immediately on conflict).
1211
+ * @default 3
1212
+ */
1213
+ maxRetries?: number;
1214
+ }
1215
+ /**
1216
+ * Default storage configuration.
1217
+ */
1218
+ declare const DEFAULT_STORAGE_CONFIG: Required<OpLogStorageConfig>;
1219
+ /**
1220
+ * Create an in-memory storage adapter.
1221
+ *
1222
+ * @example
1223
+ * ```typescript
1224
+ * const storage = memoryStorage();
1225
+ *
1226
+ * // Or with custom config
1227
+ * const storage = memoryStorage({
1228
+ * maxPatchesPerEntity: 500,
1229
+ * maxPatchAge: 60000,
1230
+ * });
1231
+ * ```
1232
+ */
1233
+ declare function memoryStorage(config?: OpLogStorageConfig): OpLogStorage;
1234
+ /**
1235
+ * Operation log plugin configuration.
1236
+ */
1237
+ interface OpLogOptions extends OpLogStorageConfig {
1238
+ /**
1239
+ * Storage adapter for state/version/patches.
1240
+ * Defaults to in-memory storage.
1241
+ *
1242
+ * @example
1243
+ * ```typescript
1244
+ * // In-memory (default)
1245
+ * opLog()
1246
+ *
1247
+ * // Redis for serverless
1248
+ * opLog({ storage: redisStorage({ url: REDIS_URL }) })
1249
+ * ```
1250
+ */
1251
+ storage?: OpLogStorage;
1252
+ /**
1253
+ * Whether to enable debug logging.
1254
+ * @default false
1255
+ */
1256
+ debug?: boolean;
1257
+ }
1258
+ /**
1259
+ * Broadcast result returned by the plugin.
1260
+ * Handler uses this to send updates to subscribers.
1261
+ */
1262
+ interface BroadcastResult {
1263
+ /** Current version after update */
1264
+ version: number;
1265
+ /** Patch operations (null if first emit or log evicted) */
1266
+ patch: PatchOperation2[] | null;
1267
+ /** Full data (for initial sends or when patch unavailable) */
1268
+ data: Record<string, unknown>;
1269
+ }
1270
+ /**
1271
+ * OpLog plugin instance type.
1272
+ */
1273
+ interface OpLogPlugin extends ServerPlugin {
1274
+ /** Get the storage adapter */
1275
+ getStorage(): OpLogStorage;
1276
+ /** Get version for an entity (async) */
1277
+ getVersion(entity: string, entityId: string): Promise<number>;
1278
+ /** Get current canonical state for an entity (async) */
1279
+ getState(entity: string, entityId: string): Promise<Record<string, unknown> | null>;
1280
+ /** Get latest patch for an entity (async) */
1281
+ getLatestPatch(entity: string, entityId: string): Promise<PatchOperation2[] | null>;
1282
+ }
1283
+ /**
1284
+ * Create an operation log plugin.
1285
+ *
1286
+ * This plugin provides cursor-based state synchronization:
1287
+ * - Canonical state per entity (server truth)
1288
+ * - Version tracking for cursor-based sync
1289
+ * - Operation log for efficient reconnection (patches or snapshot)
1290
+ *
1291
+ * This plugin does NOT handle subscription routing - that's the handler's job.
1292
+ * Memory: O(entities × history) - does not scale with client count.
1293
+ *
1294
+ * @example
1295
+ * ```typescript
1296
+ * // Default (in-memory)
1297
+ * const server = createApp({
1298
+ * router: appRouter,
1299
+ * plugins: [opLog()],
1300
+ * });
1301
+ *
1302
+ * // With Redis for serverless
1303
+ * const server = createApp({
1304
+ * router: appRouter,
1305
+ * plugins: [opLog({
1306
+ * storage: redisStorage({ url: process.env.REDIS_URL }),
1307
+ * })],
1308
+ * });
1309
+ * ```
1310
+ */
1311
+ declare function opLog(options?: OpLogOptions): OpLogPlugin;
1312
+ /**
1313
+ * Check if a plugin is an opLog plugin.
1314
+ */
1315
+ declare function isOpLogPlugin(plugin: ServerPlugin): plugin is OpLogPlugin;
1316
+ import { OptimisticPluginMarker } from "@sylphx/lens-core";
1317
+ import { isOptimisticPlugin } from "@sylphx/lens-core";
1318
+ /**
1319
+ * Optimistic plugin configuration.
1320
+ */
1321
+ interface OptimisticPluginOptions {
1322
+ /**
1323
+ * Whether to auto-derive optimistic config from mutation naming.
1324
+ * - `updateX` → "merge"
1325
+ * - `createX` / `addX` → "create"
1326
+ * - `deleteX` / `removeX` → "delete"
1327
+ * @default true
1328
+ */
1329
+ autoDerive?: boolean;
1330
+ /**
1331
+ * Enable debug logging.
1332
+ * @default false
1333
+ */
1334
+ debug?: boolean;
1335
+ }
1336
+ /**
1337
+ * Combined plugin type that works with both lens() and createApp().
1338
+ *
1339
+ * This type satisfies:
1340
+ * - OptimisticPluginMarker (RuntimePlugin<OptimisticPluginExtension>) for lens() type extensions
1341
+ * - ServerPlugin for server-side metadata processing
1342
+ */
1343
+ type OptimisticPlugin = OptimisticPluginMarker & ServerPlugin;
1344
+ /**
1345
+ * Create an optimistic plugin.
1346
+ *
1347
+ * This plugin enables type-safe .optimistic() on mutation builders when used
1348
+ * with lens(), and processes mutation definitions for server metadata.
1349
+ *
1350
+ * @example With lens() for type-safe builders
1351
+ * ```typescript
1352
+ * const { mutation, plugins } = lens<AppContext>({ plugins: [optimisticPlugin()] });
1353
+ *
1354
+ * // .optimistic() is type-safe (compile error without plugin)
1355
+ * const updateUser = mutation()
1356
+ * .input(z.object({ id: z.string(), name: z.string() }))
1357
+ * .returns(User)
1358
+ * .optimistic('merge')
1359
+ * .resolve(({ input }) => db.user.update(input));
1360
+ *
1361
+ * const server = createApp({ router, plugins });
1362
+ * ```
1363
+ *
1364
+ * @example Direct server usage
1365
+ * ```typescript
1366
+ * const server = createApp({
1367
+ * router: appRouter,
1368
+ * plugins: [optimisticPlugin()],
1369
+ * });
1370
+ * ```
1371
+ */
1372
+ declare function optimisticPlugin(options?: OptimisticPluginOptions): OptimisticPlugin;
1373
+ import { OperationLogConfig, OperationLogEntry, OperationLogStats, PatchOperation as PatchOperation3, Version } from "@sylphx/lens-core";
1374
+ /**
1375
+ * Operation log with efficient lookup and bounded memory.
1376
+ *
1377
+ * Features:
1378
+ * - O(1) lookup by entity key (via index)
1379
+ * - Automatic eviction based on count, age, and memory
1380
+ * - Version tracking for efficient reconnect
1381
+ *
1382
+ * @example
1383
+ * ```typescript
1384
+ * const log = new OperationLog({ maxEntries: 10000, maxAge: 300000 });
1385
+ *
1386
+ * // Append operation
1387
+ * log.append({
1388
+ * entityKey: "user:123",
1389
+ * version: 5,
1390
+ * timestamp: Date.now(),
1391
+ * patch: [{ op: "replace", path: "/name", value: "Alice" }],
1392
+ * patchSize: 50,
1393
+ * });
1394
+ *
1395
+ * // Get patches since version
1396
+ * const patches = log.getSince("user:123", 3);
1397
+ * // Returns patches for versions 4 and 5, or null if too old
1398
+ * ```
1399
+ */
1400
+ declare class OperationLog {
1401
+ private entries;
1402
+ private config;
1403
+ private totalMemory;
1404
+ private entityIndex;
1405
+ private oldestVersionIndex;
1406
+ private newestVersionIndex;
1407
+ private cleanupTimer;
1408
+ constructor(config?: Partial<OperationLogConfig>);
1409
+ /**
1410
+ * Append new operation to log.
1411
+ * Automatically evicts old entries if limits exceeded.
1412
+ */
1413
+ append(entry: OperationLogEntry): void;
1414
+ /**
1415
+ * Append batch of operations efficiently.
1416
+ */
1417
+ appendBatch(entries: OperationLogEntry[]): void;
1418
+ /**
1419
+ * Get all operations for entity since given version.
1420
+ * Returns null if version is too old (not in log).
1421
+ * Returns empty array if client is already at latest version.
1422
+ */
1423
+ getSince(entityKey: string, fromVersion: Version): OperationLogEntry[] | null;
1424
+ /**
1425
+ * Check if version is within log range for entity.
1426
+ */
1427
+ hasVersion(entityKey: string, version: Version): boolean;
1428
+ /**
1429
+ * Get oldest version available for entity.
1430
+ */
1431
+ getOldestVersion(entityKey: string): Version | null;
1432
+ /**
1433
+ * Get newest version for entity.
1434
+ */
1435
+ getNewestVersion(entityKey: string): Version | null;
1436
+ /**
1437
+ * Get all patches for entity (for debugging/testing).
1438
+ */
1439
+ getAll(entityKey: string): OperationLogEntry[];
1440
+ /**
1441
+ * Cleanup expired entries.
1442
+ * Called automatically on interval or manually.
1443
+ */
1444
+ cleanup(): void;
1445
+ /**
1446
+ * Remove oldest entry and update tracking.
1447
+ */
1448
+ private removeOldest;
1449
+ /**
1450
+ * Rebuild indices after cleanup.
1451
+ * O(n) operation, should be called sparingly.
1452
+ */
1453
+ private rebuildIndices;
1454
+ /**
1455
+ * Check limits and trigger cleanup if needed.
1456
+ */
1457
+ private checkLimits;
1458
+ /**
1459
+ * Get statistics about the operation log.
1460
+ */
1461
+ getStats(): OperationLogStats;
1462
+ /**
1463
+ * Clear all entries.
1464
+ */
1465
+ clear(): void;
1466
+ /**
1467
+ * Stop cleanup timer and release resources.
1468
+ */
1469
+ dispose(): void;
1470
+ /**
1471
+ * Update configuration.
1472
+ */
1473
+ updateConfig(config: Partial<OperationLogConfig>): void;
1474
+ }
1475
+ /**
1476
+ * Coalesce multiple patches into single optimized patch.
1477
+ * Removes redundant operations and combines sequential changes.
1478
+ *
1479
+ * @param patches - Array of patch arrays (one per version)
1480
+ * @returns Single coalesced patch array
1481
+ */
1482
+ declare function coalescePatches(patches: PatchOperation3[][]): PatchOperation3[];
1483
+ /**
1484
+ * Estimate memory size of patch operations.
485
1485
  */
486
- declare function createSSEHandler(config: SSEHandlerConfig): SSEHandler;
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 };
1486
+ declare function estimatePatchSize(patch: PatchOperation3[]): number;
1487
+ export { useContext, tryUseContext, runWithContextAsync, runWithContext, router, query, optimisticPlugin, opLog, mutation, memoryStorage, isOptimisticPlugin, isOpLogPlugin, hasContext, handleWebSSE, handleWebQuery, handleWebMutation, extendContext, estimatePatchSize, createWSHandler, createServerClientProxy, createSSEHandler, createPluginManager, createHandler, createHTTPHandler, createFrameworkHandler, createContext, createApp, coalescePatches, WebSocketLike, WSHandlerOptions, WSHandler, UnsubscribeContext2 as UnsubscribeContext, SubscribeContext2 as SubscribeContext, StoredPatchEntry, StoredEntityState, ServerPlugin, ServerMetadata, LensServerConfig as ServerConfig, SelectionObject, SSEHandlerConfig as SSEHandlerOptions, SSEHandlerConfig, SSEHandler, SSEClient, RouterRoutes, RouterDef3 as RouterDef, ResolverFn, ResolverContext, QueryDef2 as QueryDef, QueriesMap, PluginManager, OptimisticPluginOptions, OperationsMap, OperationMeta, OperationLog, OpLogStorageConfig, OpLogStorage, OpLogPlugin, OpLogOptions, MutationsMap, MutationDef2 as MutationDef, LensServer, LensResult, LensOperation, InferRouterContext3 as InferRouterContext, InferOutput, InferInput, InferApi, HandlerOptions, Handler, HTTPHandlerOptions, HTTPHandler, FrameworkHandlerOptions, EntitiesMap, EnhanceOperationMetaContext, EmitResult, DisconnectContext, DEFAULT_STORAGE_CONFIG, ConnectContext, ClientSendFn, BroadcastResult, BeforeSendContext, BeforeMutationContext, AfterSendContext, AfterMutationContext };