@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 +1244 -260
- package/dist/index.js +1700 -1158
- 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 +44 -0
- package/src/server/types.ts +289 -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,210 +1,441 @@
|
|
|
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 */
|
|
26
153
|
clientId: string;
|
|
27
|
-
|
|
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
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
* @param id - Entity ID
|
|
144
|
-
* @param operation - Array operation to apply
|
|
349
|
+
* @returns BroadcastResult with version, patch, and data
|
|
145
350
|
*/
|
|
146
|
-
|
|
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
|
-
*
|
|
367
|
+
* Register a plugin.
|
|
149
368
|
*/
|
|
150
|
-
|
|
369
|
+
register(plugin: ServerPlugin): void;
|
|
151
370
|
/**
|
|
152
|
-
*
|
|
153
|
-
* Computes optimal diff strategy.
|
|
371
|
+
* Get all registered plugins.
|
|
154
372
|
*/
|
|
155
|
-
|
|
373
|
+
getPlugins(): readonly ServerPlugin[];
|
|
156
374
|
/**
|
|
157
|
-
*
|
|
375
|
+
* Run onConnect hooks.
|
|
376
|
+
* Returns false if any plugin rejects the connection.
|
|
158
377
|
*/
|
|
159
|
-
|
|
378
|
+
runOnConnect(ctx: ConnectContext): Promise<boolean>;
|
|
160
379
|
/**
|
|
161
|
-
*
|
|
380
|
+
* Run onDisconnect hooks.
|
|
162
381
|
*/
|
|
163
|
-
|
|
382
|
+
runOnDisconnect(ctx: DisconnectContext): Promise<void>;
|
|
164
383
|
/**
|
|
165
|
-
*
|
|
384
|
+
* Run onSubscribe hooks.
|
|
385
|
+
* Returns false if any plugin rejects the subscription.
|
|
166
386
|
*/
|
|
167
|
-
|
|
387
|
+
runOnSubscribe(ctx: SubscribeContext2): Promise<boolean>;
|
|
168
388
|
/**
|
|
169
|
-
*
|
|
389
|
+
* Run onUnsubscribe hooks.
|
|
170
390
|
*/
|
|
171
|
-
|
|
391
|
+
runOnUnsubscribe(ctx: UnsubscribeContext2): Promise<void>;
|
|
172
392
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
393
|
+
* Run beforeSend hooks.
|
|
394
|
+
* Each plugin can modify the data.
|
|
175
395
|
*/
|
|
176
|
-
|
|
396
|
+
runBeforeSend(ctx: BeforeSendContext): Promise<Record<string, unknown>>;
|
|
177
397
|
/**
|
|
178
|
-
*
|
|
179
|
-
* Computes optimal transfer strategy for each field.
|
|
398
|
+
* Run afterSend hooks.
|
|
180
399
|
*/
|
|
181
|
-
|
|
400
|
+
runAfterSend(ctx: AfterSendContext): Promise<void>;
|
|
182
401
|
/**
|
|
183
|
-
*
|
|
402
|
+
* Run beforeMutation hooks.
|
|
403
|
+
* Returns false if any plugin rejects the mutation.
|
|
184
404
|
*/
|
|
185
|
-
|
|
405
|
+
runBeforeMutation(ctx: BeforeMutationContext): Promise<boolean>;
|
|
186
406
|
/**
|
|
187
|
-
*
|
|
407
|
+
* Run afterMutation hooks.
|
|
188
408
|
*/
|
|
189
|
-
|
|
190
|
-
private makeKey;
|
|
409
|
+
runAfterMutation(ctx: AfterMutationContext): Promise<void>;
|
|
191
410
|
/**
|
|
192
|
-
*
|
|
411
|
+
* Run onReconnect hooks.
|
|
412
|
+
* Returns the first non-null result from a plugin.
|
|
193
413
|
*/
|
|
194
|
-
|
|
195
|
-
clients: number;
|
|
196
|
-
entities: number;
|
|
197
|
-
totalSubscriptions: number;
|
|
198
|
-
};
|
|
414
|
+
runOnReconnect(ctx: ReconnectContext2): Promise<ReconnectHookResult2[] | null>;
|
|
199
415
|
/**
|
|
200
|
-
*
|
|
416
|
+
* Run onUpdateFields hooks.
|
|
201
417
|
*/
|
|
202
|
-
|
|
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
|
|
435
|
+
* Create a new plugin manager.
|
|
206
436
|
*/
|
|
207
|
-
declare function
|
|
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?:
|
|
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
|
|
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
|
|
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)
|
|
473
|
+
/** Router definition (namespaced operations) */
|
|
243
474
|
router?: TRouter | undefined;
|
|
244
|
-
/** Query definitions (flat
|
|
475
|
+
/** Query definitions (flat) */
|
|
245
476
|
queries?: QueriesMap | undefined;
|
|
246
|
-
/** Mutation definitions (flat
|
|
477
|
+
/** Mutation definitions (flat) */
|
|
247
478
|
mutations?: MutationsMap | undefined;
|
|
248
|
-
/** Field resolvers array
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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 */
|
|
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
|
-
*
|
|
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
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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
|
-
|
|
356
|
-
context?: (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
* //
|
|
385
|
-
* const
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
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
|
|
395
|
-
TRouter extends
|
|
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:
|
|
652
|
+
context: InferRouterContext2<TRouter>;
|
|
404
653
|
};
|
|
405
654
|
};
|
|
406
|
-
declare function
|
|
407
|
-
TContext extends
|
|
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
|
|
425
|
-
interface
|
|
681
|
+
/** SSE client handle for sending messages */
|
|
682
|
+
interface SSEClient {
|
|
683
|
+
/** Unique client ID */
|
|
426
684
|
id: string;
|
|
427
|
-
|
|
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
|
|
693
|
+
* Pure SSE transport handler.
|
|
431
694
|
*
|
|
432
|
-
* This
|
|
433
|
-
* -
|
|
434
|
-
* -
|
|
435
|
-
* -
|
|
695
|
+
* This handler ONLY manages:
|
|
696
|
+
* - SSE connection lifecycle
|
|
697
|
+
* - Message sending to clients
|
|
698
|
+
* - Heartbeat keepalive
|
|
436
699
|
*
|
|
437
|
-
*
|
|
700
|
+
* It does NOT know about:
|
|
701
|
+
* - State management
|
|
702
|
+
* - Subscriptions
|
|
703
|
+
* - Plugins
|
|
438
704
|
*
|
|
439
705
|
* @example
|
|
440
706
|
* ```typescript
|
|
441
|
-
* const
|
|
442
|
-
*
|
|
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
|
-
* //
|
|
448
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
487
|
-
export { router, query, mutation,
|
|
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 };
|