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