@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 +1262 -262
- package/dist/index.js +1714 -1154
- package/package.json +2 -2
- package/src/context/index.test.ts +425 -0
- package/src/context/index.ts +90 -0
- package/src/e2e/server.test.ts +215 -433
- package/src/handlers/framework.ts +294 -0
- package/src/handlers/http.test.ts +215 -0
- package/src/handlers/http.ts +189 -0
- package/src/handlers/index.ts +55 -0
- package/src/handlers/unified.ts +114 -0
- package/src/handlers/ws-types.ts +126 -0
- package/src/handlers/ws.ts +669 -0
- package/src/index.ts +127 -24
- package/src/plugin/index.ts +41 -0
- package/src/plugin/op-log.ts +286 -0
- package/src/plugin/optimistic.ts +375 -0
- package/src/plugin/types.ts +551 -0
- package/src/reconnect/index.ts +9 -0
- package/src/reconnect/operation-log.test.ts +480 -0
- package/src/reconnect/operation-log.ts +450 -0
- package/src/server/create.test.ts +256 -2193
- package/src/server/create.ts +285 -1481
- package/src/server/dataloader.ts +60 -0
- package/src/server/selection.ts +123 -0
- package/src/server/types.ts +306 -0
- package/src/sse/handler.ts +123 -56
- package/src/state/index.ts +9 -11
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +279 -0
- package/src/storage/types.ts +205 -0
- package/src/state/graph-state-manager.test.ts +0 -1105
- package/src/state/graph-state-manager.ts +0 -890
package/dist/index.d.ts
CHANGED
|
@@ -1,215 +1,462 @@
|
|
|
1
|
-
import { InferRouterContext as
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Context passed to onBroadcast hook.
|
|
139
|
+
*/
|
|
140
|
+
interface BroadcastContext {
|
|
141
|
+
/** Entity type name */
|
|
20
142
|
entity: string;
|
|
21
|
-
|
|
143
|
+
/** Entity ID */
|
|
144
|
+
entityId: string;
|
|
145
|
+
/** Entity data */
|
|
22
146
|
data: Record<string, unknown>;
|
|
23
147
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* });
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
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
|
-
*
|
|
282
|
+
* Called when a client connects.
|
|
283
|
+
* Can return false to reject the connection.
|
|
73
284
|
*/
|
|
74
|
-
|
|
285
|
+
onConnect?: (ctx: ConnectContext) => void | boolean | Promise<void | boolean>;
|
|
75
286
|
/**
|
|
76
|
-
*
|
|
287
|
+
* Called when a client disconnects.
|
|
77
288
|
*/
|
|
78
|
-
|
|
289
|
+
onDisconnect?: (ctx: DisconnectContext) => void | Promise<void>;
|
|
79
290
|
/**
|
|
80
|
-
*
|
|
291
|
+
* Called when a client subscribes to an operation.
|
|
292
|
+
* Can modify the context or return false to reject.
|
|
81
293
|
*/
|
|
82
|
-
|
|
294
|
+
onSubscribe?: (ctx: SubscribeContext2) => void | boolean | Promise<void | boolean>;
|
|
83
295
|
/**
|
|
84
|
-
*
|
|
296
|
+
* Called when a client unsubscribes.
|
|
85
297
|
*/
|
|
86
|
-
|
|
298
|
+
onUnsubscribe?: (ctx: UnsubscribeContext2) => void | Promise<void>;
|
|
87
299
|
/**
|
|
88
|
-
*
|
|
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
|
-
|
|
305
|
+
beforeSend?: (ctx: BeforeSendContext) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;
|
|
91
306
|
/**
|
|
92
|
-
*
|
|
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
|
-
|
|
101
|
-
replace?: boolean;
|
|
102
|
-
}): void;
|
|
309
|
+
afterSend?: (ctx: AfterSendContext) => void | Promise<void>;
|
|
103
310
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
-
|
|
314
|
+
beforeMutation?: (ctx: BeforeMutationContext) => void | boolean | Promise<void | boolean>;
|
|
113
315
|
/**
|
|
114
|
-
*
|
|
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
|
-
|
|
318
|
+
afterMutation?: (ctx: AfterMutationContext) => void | Promise<void>;
|
|
122
319
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
320
|
+
* Called when a client reconnects with subscription state.
|
|
321
|
+
* Plugin can return sync results for each subscription.
|
|
125
322
|
*
|
|
126
|
-
* @
|
|
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
|
-
|
|
325
|
+
onReconnect?: (ctx: ReconnectContext2) => ReconnectHookResult2[] | null | Promise<ReconnectHookResult2[] | null>;
|
|
131
326
|
/**
|
|
132
|
-
*
|
|
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
|
-
* @
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
|
|
343
|
+
enhanceOperationMeta?: (ctx: EnhanceOperationMetaContext) => void;
|
|
139
344
|
/**
|
|
140
|
-
*
|
|
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
|
-
* @
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
369
|
+
register(plugin: ServerPlugin): void;
|
|
147
370
|
/**
|
|
148
|
-
*
|
|
371
|
+
* Get all registered plugins.
|
|
149
372
|
*/
|
|
150
|
-
|
|
373
|
+
getPlugins(): readonly ServerPlugin[];
|
|
151
374
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
375
|
+
* Run onConnect hooks.
|
|
376
|
+
* Returns false if any plugin rejects the connection.
|
|
154
377
|
*/
|
|
155
|
-
|
|
378
|
+
runOnConnect(ctx: ConnectContext): Promise<boolean>;
|
|
156
379
|
/**
|
|
157
|
-
*
|
|
380
|
+
* Run onDisconnect hooks.
|
|
158
381
|
*/
|
|
159
|
-
|
|
382
|
+
runOnDisconnect(ctx: DisconnectContext): Promise<void>;
|
|
160
383
|
/**
|
|
161
|
-
*
|
|
384
|
+
* Run onSubscribe hooks.
|
|
385
|
+
* Returns false if any plugin rejects the subscription.
|
|
162
386
|
*/
|
|
163
|
-
|
|
387
|
+
runOnSubscribe(ctx: SubscribeContext2): Promise<boolean>;
|
|
164
388
|
/**
|
|
165
|
-
*
|
|
389
|
+
* Run onUnsubscribe hooks.
|
|
166
390
|
*/
|
|
167
|
-
|
|
391
|
+
runOnUnsubscribe(ctx: UnsubscribeContext2): Promise<void>;
|
|
168
392
|
/**
|
|
169
|
-
*
|
|
393
|
+
* Run beforeSend hooks.
|
|
394
|
+
* Each plugin can modify the data.
|
|
170
395
|
*/
|
|
171
|
-
|
|
396
|
+
runBeforeSend(ctx: BeforeSendContext): Promise<Record<string, unknown>>;
|
|
172
397
|
/**
|
|
173
|
-
*
|
|
174
|
-
* Computes optimal transfer strategy.
|
|
398
|
+
* Run afterSend hooks.
|
|
175
399
|
*/
|
|
176
|
-
|
|
400
|
+
runAfterSend(ctx: AfterSendContext): Promise<void>;
|
|
177
401
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
402
|
+
* Run beforeMutation hooks.
|
|
403
|
+
* Returns false if any plugin rejects the mutation.
|
|
180
404
|
*/
|
|
181
|
-
|
|
405
|
+
runBeforeMutation(ctx: BeforeMutationContext): Promise<boolean>;
|
|
182
406
|
/**
|
|
183
|
-
*
|
|
407
|
+
* Run afterMutation hooks.
|
|
184
408
|
*/
|
|
185
|
-
|
|
409
|
+
runAfterMutation(ctx: AfterMutationContext): Promise<void>;
|
|
186
410
|
/**
|
|
187
|
-
*
|
|
411
|
+
* Run onReconnect hooks.
|
|
412
|
+
* Returns the first non-null result from a plugin.
|
|
188
413
|
*/
|
|
189
|
-
|
|
190
|
-
private makeKey;
|
|
414
|
+
runOnReconnect(ctx: ReconnectContext2): Promise<ReconnectHookResult2[] | null>;
|
|
191
415
|
/**
|
|
192
|
-
*
|
|
416
|
+
* Run onUpdateFields hooks.
|
|
193
417
|
*/
|
|
194
|
-
|
|
195
|
-
clients: number;
|
|
196
|
-
entities: number;
|
|
197
|
-
totalSubscriptions: number;
|
|
198
|
-
};
|
|
418
|
+
runOnUpdateFields(ctx: UpdateFieldsContext2): Promise<void>;
|
|
199
419
|
/**
|
|
200
|
-
*
|
|
420
|
+
* Run enhanceOperationMeta hooks.
|
|
421
|
+
* Each plugin can add fields to the operation metadata.
|
|
201
422
|
*/
|
|
202
|
-
|
|
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
|
|
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?:
|
|
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
|
|
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
|
|
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)
|
|
489
|
+
/** Router definition (namespaced operations) */
|
|
243
490
|
router?: TRouter | undefined;
|
|
244
|
-
/** Query definitions (flat
|
|
491
|
+
/** Query definitions (flat) */
|
|
245
492
|
queries?: QueriesMap | undefined;
|
|
246
|
-
/** Mutation definitions (flat
|
|
493
|
+
/** Mutation definitions (flat) */
|
|
247
494
|
mutations?: MutationsMap | undefined;
|
|
248
|
-
/** Field resolvers array
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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
|
-
|
|
356
|
-
context?: (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
* //
|
|
385
|
-
* const
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
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
|
|
395
|
-
TRouter extends
|
|
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:
|
|
668
|
+
context: InferRouterContext2<TRouter>;
|
|
404
669
|
};
|
|
405
670
|
};
|
|
406
|
-
declare function
|
|
407
|
-
TContext extends
|
|
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
|
|
425
|
-
interface
|
|
697
|
+
/** SSE client handle for sending messages */
|
|
698
|
+
interface SSEClient {
|
|
699
|
+
/** Unique client ID */
|
|
426
700
|
id: string;
|
|
427
|
-
|
|
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
|
|
709
|
+
* Pure SSE transport handler.
|
|
431
710
|
*
|
|
432
|
-
* This
|
|
433
|
-
* -
|
|
434
|
-
* -
|
|
435
|
-
* -
|
|
711
|
+
* This handler ONLY manages:
|
|
712
|
+
* - SSE connection lifecycle
|
|
713
|
+
* - Message sending to clients
|
|
714
|
+
* - Heartbeat keepalive
|
|
436
715
|
*
|
|
437
|
-
*
|
|
716
|
+
* It does NOT know about:
|
|
717
|
+
* - State management
|
|
718
|
+
* - Subscriptions
|
|
719
|
+
* - Plugins
|
|
438
720
|
*
|
|
439
721
|
* @example
|
|
440
722
|
* ```typescript
|
|
441
|
-
* const
|
|
442
|
-
*
|
|
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
|
-
* //
|
|
448
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
487
|
-
export { router, query, mutation,
|
|
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 };
|