@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.js CHANGED
@@ -5,524 +5,168 @@ import {
5
5
  router
6
6
  } from "@sylphx/lens-core";
7
7
 
8
+ // src/context/index.ts
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ var globalContextStore = new AsyncLocalStorage;
11
+ function createContext() {
12
+ return globalContextStore;
13
+ }
14
+ function useContext() {
15
+ const ctx = globalContextStore.getStore();
16
+ if (!ctx) {
17
+ throw new Error("useContext() called outside of context. " + "Make sure to wrap your code with runWithContext() or use explicit ctx parameter.");
18
+ }
19
+ return ctx;
20
+ }
21
+ function tryUseContext() {
22
+ return globalContextStore.getStore();
23
+ }
24
+ function runWithContext(_context, value, fn) {
25
+ return globalContextStore.run(value, fn);
26
+ }
27
+ async function runWithContextAsync(context, value, fn) {
28
+ return runWithContext(context, value, fn);
29
+ }
30
+ function hasContext() {
31
+ return globalContextStore.getStore() !== undefined;
32
+ }
33
+ function extendContext(current, extension) {
34
+ return { ...current, ...extension };
35
+ }
8
36
  // src/server/create.ts
9
37
  import {
10
- createContext,
11
38
  createEmit,
12
- createUpdate as createUpdate2,
13
39
  flattenRouter,
14
40
  isEntityDef,
15
41
  isMutationDef,
16
- isPipeline,
17
42
  isQueryDef,
18
- runWithContext,
19
43
  toResolverMap
20
44
  } from "@sylphx/lens-core";
21
45
 
22
- // src/state/graph-state-manager.ts
23
- import {
24
- applyUpdate,
25
- computeArrayDiff,
26
- createUpdate,
27
- makeEntityKey
28
- } from "@sylphx/lens-core";
29
-
30
- class GraphStateManager {
31
- clients = new Map;
32
- canonical = new Map;
33
- canonicalArrays = new Map;
34
- clientStates = new Map;
35
- clientArrayStates = new Map;
36
- entitySubscribers = new Map;
37
- config;
38
- constructor(config = {}) {
39
- this.config = config;
40
- }
41
- addClient(client) {
42
- this.clients.set(client.id, client);
43
- this.clientStates.set(client.id, new Map);
44
- this.clientArrayStates.set(client.id, new Map);
45
- }
46
- removeClient(clientId) {
47
- for (const [key, subscribers] of this.entitySubscribers) {
48
- subscribers.delete(clientId);
49
- if (subscribers.size === 0) {
50
- this.cleanupEntity(key);
51
- }
52
- }
53
- this.clients.delete(clientId);
54
- this.clientStates.delete(clientId);
55
- this.clientArrayStates.delete(clientId);
56
- }
57
- subscribe(clientId, entity, id, fields = "*") {
58
- const key = this.makeKey(entity, id);
59
- let subscribers = this.entitySubscribers.get(key);
60
- if (!subscribers) {
61
- subscribers = new Set;
62
- this.entitySubscribers.set(key, subscribers);
63
- }
64
- subscribers.add(clientId);
65
- const clientStateMap = this.clientStates.get(clientId);
66
- if (clientStateMap) {
67
- const fieldSet = fields === "*" ? "*" : new Set(fields);
68
- clientStateMap.set(key, {
69
- lastState: {},
70
- fields: fieldSet
71
- });
72
- }
73
- const canonicalState = this.canonical.get(key);
74
- if (canonicalState) {
75
- this.sendInitialData(clientId, entity, id, canonicalState, fields);
76
- }
77
- }
78
- unsubscribe(clientId, entity, id) {
79
- const key = this.makeKey(entity, id);
80
- const subscribers = this.entitySubscribers.get(key);
81
- if (subscribers) {
82
- subscribers.delete(clientId);
83
- if (subscribers.size === 0) {
84
- this.cleanupEntity(key);
46
+ // src/plugin/types.ts
47
+ class PluginManager {
48
+ plugins = [];
49
+ register(plugin) {
50
+ this.plugins.push(plugin);
51
+ }
52
+ getPlugins() {
53
+ return this.plugins;
54
+ }
55
+ async runOnConnect(ctx) {
56
+ for (const plugin of this.plugins) {
57
+ if (plugin.onConnect) {
58
+ const result = await plugin.onConnect(ctx);
59
+ if (result === false)
60
+ return false;
85
61
  }
86
62
  }
87
- const clientStateMap = this.clientStates.get(clientId);
88
- if (clientStateMap) {
89
- clientStateMap.delete(key);
90
- }
63
+ return true;
91
64
  }
92
- updateSubscription(clientId, entity, id, fields) {
93
- const key = this.makeKey(entity, id);
94
- const clientStateMap = this.clientStates.get(clientId);
95
- if (clientStateMap) {
96
- const state = clientStateMap.get(key);
97
- if (state) {
98
- state.fields = fields === "*" ? "*" : new Set(fields);
65
+ async runOnDisconnect(ctx) {
66
+ for (const plugin of this.plugins) {
67
+ if (plugin.onDisconnect) {
68
+ await plugin.onDisconnect(ctx);
99
69
  }
100
70
  }
101
71
  }
102
- emit(entity, id, data, options = {}) {
103
- const key = this.makeKey(entity, id);
104
- let currentCanonical = this.canonical.get(key);
105
- if (options.replace || !currentCanonical) {
106
- currentCanonical = { ...data };
107
- } else {
108
- currentCanonical = { ...currentCanonical, ...data };
109
- }
110
- this.canonical.set(key, currentCanonical);
111
- const subscribers = this.entitySubscribers.get(key);
112
- if (!subscribers)
113
- return;
114
- for (const clientId of subscribers) {
115
- this.pushToClient(clientId, entity, id, key, currentCanonical);
116
- }
117
- }
118
- emitField(entity, id, field, update) {
119
- const key = this.makeKey(entity, id);
120
- let currentCanonical = this.canonical.get(key);
121
- if (!currentCanonical) {
122
- currentCanonical = {};
123
- }
124
- const oldValue = currentCanonical[field];
125
- const newValue = applyUpdate(oldValue, update);
126
- currentCanonical = { ...currentCanonical, [field]: newValue };
127
- this.canonical.set(key, currentCanonical);
128
- const subscribers = this.entitySubscribers.get(key);
129
- if (!subscribers)
130
- return;
131
- for (const clientId of subscribers) {
132
- this.pushFieldToClient(clientId, entity, id, key, field, newValue);
133
- }
134
- }
135
- emitBatch(entity, id, updates) {
136
- const key = this.makeKey(entity, id);
137
- let currentCanonical = this.canonical.get(key);
138
- if (!currentCanonical) {
139
- currentCanonical = {};
140
- }
141
- const changedFields = [];
142
- for (const { field, update } of updates) {
143
- const oldValue = currentCanonical[field];
144
- const newValue = applyUpdate(oldValue, update);
145
- currentCanonical[field] = newValue;
146
- changedFields.push(field);
147
- }
148
- this.canonical.set(key, currentCanonical);
149
- const subscribers = this.entitySubscribers.get(key);
150
- if (!subscribers)
151
- return;
152
- for (const clientId of subscribers) {
153
- this.pushFieldsToClient(clientId, entity, id, key, changedFields, currentCanonical);
154
- }
155
- }
156
- processCommand(entity, id, command) {
157
- switch (command.type) {
158
- case "full":
159
- this.emit(entity, id, command.data, {
160
- replace: command.replace
161
- });
162
- break;
163
- case "field":
164
- this.emitField(entity, id, command.field, command.update);
165
- break;
166
- case "batch":
167
- this.emitBatch(entity, id, command.updates);
168
- break;
169
- case "array":
170
- this.emitArrayOperation(entity, id, command.operation);
171
- break;
72
+ async runOnSubscribe(ctx) {
73
+ for (const plugin of this.plugins) {
74
+ if (plugin.onSubscribe) {
75
+ const result = await plugin.onSubscribe(ctx);
76
+ if (result === false)
77
+ return false;
78
+ }
172
79
  }
80
+ return true;
173
81
  }
174
- emitArray(entity, id, items) {
175
- const key = this.makeKey(entity, id);
176
- this.canonicalArrays.set(key, [...items]);
177
- const subscribers = this.entitySubscribers.get(key);
178
- if (!subscribers)
179
- return;
180
- for (const clientId of subscribers) {
181
- this.pushArrayToClient(clientId, entity, id, key, items);
82
+ async runOnUnsubscribe(ctx) {
83
+ for (const plugin of this.plugins) {
84
+ if (plugin.onUnsubscribe) {
85
+ await plugin.onUnsubscribe(ctx);
86
+ }
182
87
  }
183
88
  }
184
- emitArrayOperation(entity, id, operation) {
185
- const key = this.makeKey(entity, id);
186
- let currentArray = this.canonicalArrays.get(key);
187
- if (!currentArray) {
188
- currentArray = [];
189
- }
190
- const newArray = this.applyArrayOperation([...currentArray], operation);
191
- this.canonicalArrays.set(key, newArray);
192
- const subscribers = this.entitySubscribers.get(key);
193
- if (!subscribers)
194
- return;
195
- for (const clientId of subscribers) {
196
- this.pushArrayToClient(clientId, entity, id, key, newArray);
197
- }
198
- }
199
- applyArrayOperation(array, operation) {
200
- switch (operation.op) {
201
- case "push":
202
- return [...array, operation.item];
203
- case "unshift":
204
- return [operation.item, ...array];
205
- case "insert":
206
- return [
207
- ...array.slice(0, operation.index),
208
- operation.item,
209
- ...array.slice(operation.index)
210
- ];
211
- case "remove":
212
- return [...array.slice(0, operation.index), ...array.slice(operation.index + 1)];
213
- case "removeById": {
214
- const idx = array.findIndex((item) => typeof item === "object" && item !== null && ("id" in item) && item.id === operation.id);
215
- if (idx === -1)
216
- return array;
217
- return [...array.slice(0, idx), ...array.slice(idx + 1)];
218
- }
219
- case "update":
220
- return array.map((item, i) => i === operation.index ? operation.item : item);
221
- case "updateById":
222
- return array.map((item) => typeof item === "object" && item !== null && ("id" in item) && item.id === operation.id ? operation.item : item);
223
- case "merge":
224
- return array.map((item, i) => i === operation.index && typeof item === "object" && item !== null ? { ...item, ...operation.partial } : item);
225
- case "mergeById":
226
- return array.map((item) => typeof item === "object" && item !== null && ("id" in item) && item.id === operation.id ? { ...item, ...operation.partial } : item);
227
- default:
228
- return array;
229
- }
230
- }
231
- pushArrayToClient(clientId, entity, id, key, newArray) {
232
- const client = this.clients.get(clientId);
233
- if (!client)
234
- return;
235
- const clientArrayStateMap = this.clientArrayStates.get(clientId);
236
- if (!clientArrayStateMap)
237
- return;
238
- let clientArrayState = clientArrayStateMap.get(key);
239
- if (!clientArrayState) {
240
- clientArrayState = { lastState: [] };
241
- clientArrayStateMap.set(key, clientArrayState);
242
- }
243
- const { lastState } = clientArrayState;
244
- if (JSON.stringify(lastState) === JSON.stringify(newArray)) {
245
- return;
246
- }
247
- const diff = computeArrayDiff(lastState, newArray);
248
- if (diff === null || diff.length === 0) {
249
- client.send({
250
- type: "update",
251
- entity,
252
- id,
253
- updates: {
254
- _items: { strategy: "value", data: newArray }
255
- }
256
- });
257
- } else if (diff.length === 1 && diff[0].op === "replace") {
258
- client.send({
259
- type: "update",
260
- entity,
261
- id,
262
- updates: {
263
- _items: { strategy: "value", data: newArray }
264
- }
265
- });
266
- } else {
267
- client.send({
268
- type: "update",
269
- entity,
270
- id,
271
- updates: {
272
- _items: { strategy: "array", data: diff }
89
+ async runBeforeSend(ctx) {
90
+ let data = ctx.data;
91
+ for (const plugin of this.plugins) {
92
+ if (plugin.beforeSend) {
93
+ const result = await plugin.beforeSend({ ...ctx, data });
94
+ if (result !== undefined) {
95
+ data = result;
273
96
  }
274
- });
275
- }
276
- clientArrayState.lastState = [...newArray];
277
- }
278
- getArrayState(entity, id) {
279
- return this.canonicalArrays.get(this.makeKey(entity, id));
280
- }
281
- getState(entity, id) {
282
- return this.canonical.get(this.makeKey(entity, id));
283
- }
284
- hasSubscribers(entity, id) {
285
- const subscribers = this.entitySubscribers.get(this.makeKey(entity, id));
286
- return subscribers !== undefined && subscribers.size > 0;
287
- }
288
- pushToClient(clientId, entity, id, key, newState) {
289
- const client = this.clients.get(clientId);
290
- if (!client)
291
- return;
292
- const clientStateMap = this.clientStates.get(clientId);
293
- if (!clientStateMap)
294
- return;
295
- const clientEntityState = clientStateMap.get(key);
296
- if (!clientEntityState)
297
- return;
298
- const { lastState, fields } = clientEntityState;
299
- const fieldsToCheck = fields === "*" ? Object.keys(newState) : Array.from(fields);
300
- const updates = {};
301
- let hasChanges = false;
302
- for (const field of fieldsToCheck) {
303
- const oldValue = lastState[field];
304
- const newValue = newState[field];
305
- if (oldValue === newValue)
306
- continue;
307
- if (typeof oldValue === "object" && typeof newValue === "object" && JSON.stringify(oldValue) === JSON.stringify(newValue)) {
308
- continue;
309
- }
310
- const update = createUpdate(oldValue, newValue);
311
- updates[field] = update;
312
- hasChanges = true;
313
- }
314
- if (!hasChanges)
315
- return;
316
- client.send({
317
- type: "update",
318
- entity,
319
- id,
320
- updates
321
- });
322
- for (const field of fieldsToCheck) {
323
- if (newState[field] !== undefined) {
324
- clientEntityState.lastState[field] = newState[field];
325
97
  }
326
98
  }
99
+ return data;
327
100
  }
328
- pushFieldToClient(clientId, entity, id, key, field, newValue) {
329
- const client = this.clients.get(clientId);
330
- if (!client)
331
- return;
332
- const clientStateMap = this.clientStates.get(clientId);
333
- if (!clientStateMap)
334
- return;
335
- const clientEntityState = clientStateMap.get(key);
336
- if (!clientEntityState)
337
- return;
338
- const { lastState, fields } = clientEntityState;
339
- if (fields !== "*" && !fields.has(field)) {
340
- return;
341
- }
342
- const oldValue = lastState[field];
343
- if (oldValue === newValue)
344
- return;
345
- if (typeof oldValue === "object" && typeof newValue === "object" && JSON.stringify(oldValue) === JSON.stringify(newValue)) {
346
- return;
347
- }
348
- const update = createUpdate(oldValue, newValue);
349
- client.send({
350
- type: "update",
351
- entity,
352
- id,
353
- updates: { [field]: update }
354
- });
355
- clientEntityState.lastState[field] = newValue;
356
- }
357
- pushFieldsToClient(clientId, entity, id, key, changedFields, newState) {
358
- const client = this.clients.get(clientId);
359
- if (!client)
360
- return;
361
- const clientStateMap = this.clientStates.get(clientId);
362
- if (!clientStateMap)
363
- return;
364
- const clientEntityState = clientStateMap.get(key);
365
- if (!clientEntityState)
366
- return;
367
- const { lastState, fields } = clientEntityState;
368
- const updates = {};
369
- let hasChanges = false;
370
- for (const field of changedFields) {
371
- if (fields !== "*" && !fields.has(field)) {
372
- continue;
373
- }
374
- const oldValue = lastState[field];
375
- const newValue = newState[field];
376
- if (oldValue === newValue)
377
- continue;
378
- if (typeof oldValue === "object" && typeof newValue === "object" && JSON.stringify(oldValue) === JSON.stringify(newValue)) {
379
- continue;
380
- }
381
- const update = createUpdate(oldValue, newValue);
382
- updates[field] = update;
383
- hasChanges = true;
384
- }
385
- if (!hasChanges)
386
- return;
387
- client.send({
388
- type: "update",
389
- entity,
390
- id,
391
- updates
392
- });
393
- for (const field of changedFields) {
394
- if (newState[field] !== undefined) {
395
- clientEntityState.lastState[field] = newState[field];
101
+ async runAfterSend(ctx) {
102
+ for (const plugin of this.plugins) {
103
+ if (plugin.afterSend) {
104
+ await plugin.afterSend(ctx);
396
105
  }
397
106
  }
398
107
  }
399
- sendInitialData(clientId, entity, id, state, fields) {
400
- const client = this.clients.get(clientId);
401
- if (!client)
402
- return;
403
- const key = this.makeKey(entity, id);
404
- const clientStateMap = this.clientStates.get(clientId);
405
- if (!clientStateMap)
406
- return;
407
- const fieldsToSend = fields === "*" ? Object.keys(state) : fields;
408
- const dataToSend = {};
409
- const updates = {};
410
- for (const field of fieldsToSend) {
411
- if (state[field] !== undefined) {
412
- dataToSend[field] = state[field];
413
- updates[field] = { strategy: "value", data: state[field] };
108
+ async runBeforeMutation(ctx) {
109
+ for (const plugin of this.plugins) {
110
+ if (plugin.beforeMutation) {
111
+ const result = await plugin.beforeMutation(ctx);
112
+ if (result === false)
113
+ return false;
414
114
  }
415
115
  }
416
- client.send({
417
- type: "update",
418
- entity,
419
- id,
420
- updates
421
- });
422
- const clientEntityState = clientStateMap.get(key);
423
- if (clientEntityState) {
424
- clientEntityState.lastState = { ...dataToSend };
425
- }
116
+ return true;
426
117
  }
427
- cleanupEntity(key) {
428
- const [entity, id] = key.split(":");
429
- if (this.config.onEntityUnsubscribed) {
430
- this.config.onEntityUnsubscribed(entity, id);
118
+ async runAfterMutation(ctx) {
119
+ for (const plugin of this.plugins) {
120
+ if (plugin.afterMutation) {
121
+ await plugin.afterMutation(ctx);
122
+ }
431
123
  }
432
- this.entitySubscribers.delete(key);
433
- }
434
- makeKey(entity, id) {
435
- return makeEntityKey(entity, id);
436
124
  }
437
- getStats() {
438
- let totalSubscriptions = 0;
439
- for (const subscribers of this.entitySubscribers.values()) {
440
- totalSubscriptions += subscribers.size;
125
+ async runOnReconnect(ctx) {
126
+ for (const plugin of this.plugins) {
127
+ if (plugin.onReconnect) {
128
+ const result = await plugin.onReconnect(ctx);
129
+ if (result !== null) {
130
+ return result;
131
+ }
132
+ }
441
133
  }
442
- return {
443
- clients: this.clients.size,
444
- entities: this.canonical.size,
445
- totalSubscriptions
446
- };
447
- }
448
- clear() {
449
- this.clients.clear();
450
- this.canonical.clear();
451
- this.canonicalArrays.clear();
452
- this.clientStates.clear();
453
- this.clientArrayStates.clear();
454
- this.entitySubscribers.clear();
455
- }
456
- }
457
- function createGraphStateManager(config) {
458
- return new GraphStateManager(config);
459
- }
460
-
461
- // src/server/create.ts
462
- function getEntityTypeName(returnSpec) {
463
- if (!returnSpec)
464
- return;
465
- if (isEntityDef(returnSpec)) {
466
- return returnSpec._name;
467
- }
468
- if (Array.isArray(returnSpec) && returnSpec.length === 1 && isEntityDef(returnSpec[0])) {
469
- return returnSpec[0]._name;
470
- }
471
- return;
472
- }
473
- function getInputFields(inputSchema) {
474
- if (!inputSchema?.shape)
475
- return [];
476
- return Object.keys(inputSchema.shape);
477
- }
478
- function sugarToPipeline(optimistic, entityType, inputFields) {
479
- if (isPipeline(optimistic)) {
480
- return optimistic;
481
- }
482
- if (!entityType) {
483
- return optimistic;
134
+ return null;
484
135
  }
485
- if (optimistic === "merge") {
486
- const args = { type: entityType };
487
- for (const field of inputFields) {
488
- args[field] = { $input: field };
136
+ async runOnUpdateFields(ctx) {
137
+ for (const plugin of this.plugins) {
138
+ if (plugin.onUpdateFields) {
139
+ await plugin.onUpdateFields(ctx);
140
+ }
489
141
  }
490
- return {
491
- $pipe: [{ $do: "entity.update", $with: args }]
492
- };
493
142
  }
494
- if (optimistic === "create") {
495
- const args = { type: entityType, id: { $temp: true } };
496
- for (const field of inputFields) {
497
- if (field !== "id") {
498
- args[field] = { $input: field };
143
+ runEnhanceOperationMeta(ctx) {
144
+ for (const plugin of this.plugins) {
145
+ if (plugin.enhanceOperationMeta) {
146
+ plugin.enhanceOperationMeta(ctx);
499
147
  }
500
148
  }
501
- return {
502
- $pipe: [{ $do: "entity.create", $with: args }]
503
- };
504
- }
505
- if (optimistic === "delete") {
506
- return {
507
- $pipe: [{ $do: "entity.delete", $with: { type: entityType, id: { $input: "id" } } }]
508
- };
509
149
  }
510
- if (typeof optimistic === "object" && optimistic !== null && "merge" in optimistic && typeof optimistic.merge === "object") {
511
- const extra = optimistic.merge;
512
- const args = { type: entityType };
513
- for (const field of inputFields) {
514
- args[field] = { $input: field };
515
- }
516
- for (const [key, value] of Object.entries(extra)) {
517
- args[key] = value;
150
+ async runOnBroadcast(ctx) {
151
+ for (const plugin of this.plugins) {
152
+ if (plugin.onBroadcast) {
153
+ const result = await plugin.onBroadcast(ctx);
154
+ if (result && typeof result === "object" && "version" in result) {
155
+ return result;
156
+ }
157
+ if (result === true) {
158
+ return { version: 0, patch: null, data: ctx.data };
159
+ }
160
+ }
518
161
  }
519
- return {
520
- $pipe: [{ $do: "entity.update", $with: args }]
521
- };
162
+ return null;
522
163
  }
523
- return optimistic;
164
+ }
165
+ function createPluginManager() {
166
+ return new PluginManager;
524
167
  }
525
168
 
169
+ // src/server/dataloader.ts
526
170
  class DataLoader {
527
171
  batchFn;
528
172
  batch = new Map;
@@ -538,16 +182,13 @@ class DataLoader {
538
182
  } else {
539
183
  this.batch.set(key, [{ resolve, reject }]);
540
184
  }
541
- this.scheduleDispatch();
185
+ if (!this.scheduled) {
186
+ this.scheduled = true;
187
+ queueMicrotask(() => this.flush());
188
+ }
542
189
  });
543
190
  }
544
- scheduleDispatch() {
545
- if (this.scheduled)
546
- return;
547
- this.scheduled = true;
548
- queueMicrotask(() => this.dispatch());
549
- }
550
- async dispatch() {
191
+ async flush() {
551
192
  this.scheduled = false;
552
193
  const batch = this.batch;
553
194
  this.batch = new Map;
@@ -556,22 +197,70 @@ class DataLoader {
556
197
  return;
557
198
  try {
558
199
  const results = await this.batchFn(keys);
559
- keys.forEach((key, index) => {
560
- const callbacks = batch.get(key);
561
- const result = results[index] ?? null;
562
- for (const { resolve } of callbacks)
200
+ let i = 0;
201
+ for (const [_key, callbacks] of batch) {
202
+ const result = results[i++];
203
+ for (const { resolve } of callbacks) {
563
204
  resolve(result);
564
- });
205
+ }
206
+ }
565
207
  } catch (error) {
566
- for (const callbacks of batch.values()) {
567
- for (const { reject } of callbacks)
568
- reject(error);
208
+ for (const [, callbacks] of batch) {
209
+ for (const { reject } of callbacks) {
210
+ reject(error instanceof Error ? error : new Error(String(error)));
211
+ }
569
212
  }
570
213
  }
571
214
  }
572
- clear() {
573
- this.batch.clear();
215
+ }
216
+
217
+ // src/server/selection.ts
218
+ function extractSelect(value) {
219
+ if (value === true)
220
+ return null;
221
+ if (typeof value !== "object" || value === null)
222
+ return null;
223
+ const obj = value;
224
+ if ("input" in obj || "select" in obj && typeof obj.select === "object") {
225
+ return obj.select ?? null;
226
+ }
227
+ if ("select" in obj && typeof obj.select === "object") {
228
+ return obj.select;
229
+ }
230
+ return value;
231
+ }
232
+ function applySelection(data, select) {
233
+ if (!data)
234
+ return data;
235
+ if (Array.isArray(data)) {
236
+ return data.map((item) => applySelection(item, select));
237
+ }
238
+ if (typeof data !== "object")
239
+ return data;
240
+ const obj = data;
241
+ const result = {};
242
+ if ("id" in obj)
243
+ result.id = obj.id;
244
+ for (const [key, value] of Object.entries(select)) {
245
+ if (!(key in obj))
246
+ continue;
247
+ if (value === true) {
248
+ result[key] = obj[key];
249
+ } else if (typeof value === "object" && value !== null) {
250
+ const nestedSelect = extractSelect(value);
251
+ if (nestedSelect) {
252
+ result[key] = applySelection(obj[key], nestedSelect);
253
+ } else {
254
+ result[key] = obj[key];
255
+ }
256
+ }
574
257
  }
258
+ return result;
259
+ }
260
+
261
+ // src/server/create.ts
262
+ function isAsyncIterable(value) {
263
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
575
264
  }
576
265
  var noopLogger = {};
577
266
 
@@ -584,11 +273,8 @@ class LensServerImpl {
584
273
  version;
585
274
  logger;
586
275
  ctx = createContext();
587
- stateManager;
588
276
  loaders = new Map;
589
- connections = new Map;
590
- connectionCounter = 0;
591
- server = null;
277
+ pluginManager;
592
278
  constructor(config) {
593
279
  const queries = { ...config.queries ?? {} };
594
280
  const mutations = { ...config.mutations ?? {} };
@@ -609,6 +295,14 @@ class LensServerImpl {
609
295
  this.contextFactory = config.context ?? (() => ({}));
610
296
  this.version = config.version ?? "1.0.0";
611
297
  this.logger = config.logger ?? noopLogger;
298
+ this.pluginManager = createPluginManager();
299
+ for (const plugin of config.plugins ?? []) {
300
+ this.pluginManager.register(plugin);
301
+ }
302
+ this.injectNames();
303
+ this.validateDefinitions();
304
+ }
305
+ injectNames() {
612
306
  for (const [name, def] of Object.entries(this.entities)) {
613
307
  if (def && typeof def === "object" && !def._name) {
614
308
  def._name = name;
@@ -617,16 +311,6 @@ class LensServerImpl {
617
311
  for (const [name, def] of Object.entries(this.mutations)) {
618
312
  if (def && typeof def === "object") {
619
313
  def._name = name;
620
- const lastSegment = name.includes(".") ? name.split(".").pop() : name;
621
- if (!def._optimistic) {
622
- if (lastSegment.startsWith("update")) {
623
- def._optimistic = "merge";
624
- } else if (lastSegment.startsWith("create") || lastSegment.startsWith("add")) {
625
- def._optimistic = "create";
626
- } else if (lastSegment.startsWith("delete") || lastSegment.startsWith("remove")) {
627
- def._optimistic = "delete";
628
- }
629
- }
630
314
  }
631
315
  }
632
316
  for (const [name, def] of Object.entries(this.queries)) {
@@ -634,9 +318,8 @@ class LensServerImpl {
634
318
  def._name = name;
635
319
  }
636
320
  }
637
- this.stateManager = new GraphStateManager({
638
- onEntityUnsubscribed: (_entity, _id) => {}
639
- });
321
+ }
322
+ validateDefinitions() {
640
323
  for (const [name, def] of Object.entries(this.queries)) {
641
324
  if (!isQueryDef(def)) {
642
325
  throw new Error(`Invalid query definition: ${name}`);
@@ -648,9 +331,6 @@ class LensServerImpl {
648
331
  }
649
332
  }
650
333
  }
651
- getStateManager() {
652
- return this.stateManager;
653
- }
654
334
  getMetadata() {
655
335
  return {
656
336
  version: this.version,
@@ -673,76 +353,645 @@ class LensServerImpl {
673
353
  return { error: error instanceof Error ? error : new Error(String(error)) };
674
354
  }
675
355
  }
676
- buildOperationsMap() {
677
- const result = {};
678
- const setNested = (path, meta) => {
679
- const parts = path.split(".");
680
- let current = result;
681
- for (let i = 0;i < parts.length - 1; i++) {
682
- const part = parts[i];
683
- if (!current[part] || "type" in current[part]) {
684
- current[part] = {};
685
- }
686
- current = current[part];
687
- }
688
- current[parts[parts.length - 1]] = meta;
689
- };
690
- for (const [name, _def] of Object.entries(this.queries)) {
691
- setNested(name, { type: "query" });
356
+ async executeQuery(name, input) {
357
+ const queryDef = this.queries[name];
358
+ if (!queryDef)
359
+ throw new Error(`Query not found: ${name}`);
360
+ let select;
361
+ let cleanInput = input;
362
+ if (input && typeof input === "object" && "$select" in input) {
363
+ const { $select, ...rest } = input;
364
+ select = $select;
365
+ cleanInput = Object.keys(rest).length > 0 ? rest : undefined;
692
366
  }
693
- for (const [name, def] of Object.entries(this.mutations)) {
694
- const meta = { type: "mutation" };
695
- if (def._optimistic) {
696
- const entityType = getEntityTypeName(def._output);
697
- const inputFields = getInputFields(def._input);
698
- meta.optimistic = sugarToPipeline(def._optimistic, entityType, inputFields);
367
+ if (queryDef._input && cleanInput !== undefined) {
368
+ const result = queryDef._input.safeParse(cleanInput);
369
+ if (!result.success) {
370
+ throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
699
371
  }
700
- setNested(name, meta);
701
372
  }
702
- return result;
373
+ const context = await this.contextFactory();
374
+ try {
375
+ return await runWithContext(this.ctx, context, async () => {
376
+ const resolver = queryDef._resolve;
377
+ if (!resolver)
378
+ throw new Error(`Query ${name} has no resolver`);
379
+ const emit = createEmit(() => {});
380
+ const onCleanup = () => () => {};
381
+ const lensContext = { ...context, emit, onCleanup };
382
+ const result = resolver({ input: cleanInput, ctx: lensContext });
383
+ let data;
384
+ if (isAsyncIterable(result)) {
385
+ for await (const value of result) {
386
+ data = value;
387
+ break;
388
+ }
389
+ if (data === undefined) {
390
+ throw new Error(`Query ${name} returned empty stream`);
391
+ }
392
+ } else {
393
+ data = await result;
394
+ }
395
+ return this.processQueryResult(name, data, select);
396
+ });
397
+ } finally {
398
+ this.clearLoaders();
399
+ }
703
400
  }
704
- handleWebSocket(ws) {
705
- const clientId = `client_${++this.connectionCounter}`;
706
- const conn = {
707
- id: clientId,
708
- ws,
709
- subscriptions: new Map
710
- };
711
- this.connections.set(clientId, conn);
712
- this.stateManager.addClient({
713
- id: clientId,
714
- send: (msg) => {
715
- ws.send(JSON.stringify(msg));
401
+ async executeMutation(name, input) {
402
+ const mutationDef = this.mutations[name];
403
+ if (!mutationDef)
404
+ throw new Error(`Mutation not found: ${name}`);
405
+ if (mutationDef._input) {
406
+ const result = mutationDef._input.safeParse(input);
407
+ if (!result.success) {
408
+ throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
716
409
  }
717
- });
410
+ }
411
+ const context = await this.contextFactory();
412
+ try {
413
+ return await runWithContext(this.ctx, context, async () => {
414
+ const resolver = mutationDef._resolve;
415
+ if (!resolver)
416
+ throw new Error(`Mutation ${name} has no resolver`);
417
+ const emit = createEmit(() => {});
418
+ const onCleanup = () => () => {};
419
+ const lensContext = { ...context, emit, onCleanup };
420
+ return await resolver({ input, ctx: lensContext });
421
+ });
422
+ } finally {
423
+ this.clearLoaders();
424
+ }
425
+ }
426
+ async processQueryResult(_operationName, data, select) {
427
+ if (!data)
428
+ return data;
429
+ const processed = await this.resolveEntityFields(data);
430
+ if (select) {
431
+ return applySelection(processed, select);
432
+ }
433
+ return processed;
434
+ }
435
+ async resolveEntityFields(data) {
436
+ if (!data || !this.resolverMap)
437
+ return data;
438
+ if (Array.isArray(data)) {
439
+ return Promise.all(data.map((item) => this.resolveEntityFields(item)));
440
+ }
441
+ if (typeof data !== "object")
442
+ return data;
443
+ const obj = data;
444
+ const typeName = this.getTypeName(obj);
445
+ if (!typeName)
446
+ return data;
447
+ const resolverDef = this.resolverMap.get(typeName);
448
+ if (!resolverDef)
449
+ return data;
450
+ const result = { ...obj };
451
+ for (const fieldName of resolverDef.getFieldNames()) {
452
+ const field = String(fieldName);
453
+ if (resolverDef.isExposed(field))
454
+ continue;
455
+ const existingValue = result[field];
456
+ if (existingValue !== undefined) {
457
+ result[field] = await this.resolveEntityFields(existingValue);
458
+ continue;
459
+ }
460
+ const loaderKey = `${typeName}.${field}`;
461
+ const loader = this.getOrCreateLoaderForField(loaderKey, resolverDef, field);
462
+ result[field] = await loader.load(obj);
463
+ result[field] = await this.resolveEntityFields(result[field]);
464
+ }
465
+ return result;
466
+ }
467
+ getTypeName(obj) {
468
+ if ("__typename" in obj)
469
+ return obj.__typename;
470
+ if ("_type" in obj)
471
+ return obj._type;
472
+ for (const [name, def] of Object.entries(this.entities)) {
473
+ if (isEntityDef(def) && this.matchesEntity(obj, def)) {
474
+ return name;
475
+ }
476
+ }
477
+ return;
478
+ }
479
+ matchesEntity(obj, entityDef) {
480
+ return "id" in obj || entityDef._name in obj;
481
+ }
482
+ getOrCreateLoaderForField(loaderKey, resolverDef, fieldName) {
483
+ let loader = this.loaders.get(loaderKey);
484
+ if (!loader) {
485
+ loader = new DataLoader(async (parents) => {
486
+ const results = [];
487
+ for (const parent of parents) {
488
+ try {
489
+ const result = await resolverDef.resolveField(fieldName, parent, {}, {});
490
+ results.push(result);
491
+ } catch {
492
+ results.push(null);
493
+ }
494
+ }
495
+ return results;
496
+ });
497
+ this.loaders.set(loaderKey, loader);
498
+ }
499
+ return loader;
500
+ }
501
+ clearLoaders() {
502
+ this.loaders.clear();
503
+ }
504
+ buildOperationsMap() {
505
+ const result = {};
506
+ const setNested = (path, meta) => {
507
+ const parts = path.split(".");
508
+ let current = result;
509
+ for (let i = 0;i < parts.length - 1; i++) {
510
+ const part = parts[i];
511
+ if (!current[part] || "type" in current[part]) {
512
+ current[part] = {};
513
+ }
514
+ current = current[part];
515
+ }
516
+ current[parts[parts.length - 1]] = meta;
517
+ };
518
+ for (const [name, def] of Object.entries(this.queries)) {
519
+ const meta = { type: "query" };
520
+ this.pluginManager.runEnhanceOperationMeta({
521
+ path: name,
522
+ type: "query",
523
+ meta,
524
+ definition: def
525
+ });
526
+ setNested(name, meta);
527
+ }
528
+ for (const [name, def] of Object.entries(this.mutations)) {
529
+ const meta = { type: "mutation" };
530
+ this.pluginManager.runEnhanceOperationMeta({
531
+ path: name,
532
+ type: "mutation",
533
+ meta,
534
+ definition: def
535
+ });
536
+ setNested(name, meta);
537
+ }
538
+ return result;
539
+ }
540
+ async addClient(clientId, send) {
541
+ const allowed = await this.pluginManager.runOnConnect({
542
+ clientId,
543
+ send: (msg) => send(msg)
544
+ });
545
+ return allowed;
546
+ }
547
+ removeClient(clientId, subscriptionCount) {
548
+ this.pluginManager.runOnDisconnect({ clientId, subscriptionCount });
549
+ }
550
+ async subscribe(ctx) {
551
+ return this.pluginManager.runOnSubscribe(ctx);
552
+ }
553
+ unsubscribe(ctx) {
554
+ this.pluginManager.runOnUnsubscribe(ctx);
555
+ }
556
+ async broadcast(entity, entityId, data) {
557
+ await this.pluginManager.runOnBroadcast({ entity, entityId, data });
558
+ }
559
+ async send(clientId, subscriptionId, entity, entityId, data, isInitial) {
560
+ const transformedData = await this.pluginManager.runBeforeSend({
561
+ clientId,
562
+ subscriptionId,
563
+ entity,
564
+ entityId,
565
+ data,
566
+ isInitial,
567
+ fields: "*"
568
+ });
569
+ await this.pluginManager.runAfterSend({
570
+ clientId,
571
+ subscriptionId,
572
+ entity,
573
+ entityId,
574
+ data: transformedData,
575
+ isInitial,
576
+ fields: "*",
577
+ timestamp: Date.now()
578
+ });
579
+ }
580
+ async handleReconnect(ctx) {
581
+ return this.pluginManager.runOnReconnect(ctx);
582
+ }
583
+ async updateFields(ctx) {
584
+ await this.pluginManager.runOnUpdateFields(ctx);
585
+ }
586
+ getPluginManager() {
587
+ return this.pluginManager;
588
+ }
589
+ }
590
+ function createApp(config) {
591
+ const server = new LensServerImpl(config);
592
+ return server;
593
+ }
594
+ // src/sse/handler.ts
595
+ class SSEHandler {
596
+ heartbeatInterval;
597
+ onConnectCallback;
598
+ onDisconnectCallback;
599
+ clients = new Map;
600
+ clientCounter = 0;
601
+ constructor(config = {}) {
602
+ this.heartbeatInterval = config.heartbeatInterval ?? 30000;
603
+ this.onConnectCallback = config.onConnect;
604
+ this.onDisconnectCallback = config.onDisconnect;
605
+ }
606
+ handleConnection(_req) {
607
+ const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
608
+ const encoder = new TextEncoder;
609
+ const stream = new ReadableStream({
610
+ start: (controller) => {
611
+ const heartbeat = setInterval(() => {
612
+ try {
613
+ controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}
614
+
615
+ `));
616
+ } catch {
617
+ this.removeClient(clientId);
618
+ }
619
+ }, this.heartbeatInterval);
620
+ this.clients.set(clientId, { controller, heartbeat, encoder });
621
+ controller.enqueue(encoder.encode(`event: connected
622
+ data: ${JSON.stringify({ clientId })}
623
+
624
+ `));
625
+ const client = {
626
+ id: clientId,
627
+ send: (message) => this.send(clientId, message),
628
+ sendEvent: (event, data) => this.sendEvent(clientId, event, data),
629
+ close: () => this.closeClient(clientId)
630
+ };
631
+ this.onConnectCallback?.(client);
632
+ },
633
+ cancel: () => {
634
+ this.removeClient(clientId);
635
+ }
636
+ });
637
+ return new Response(stream, {
638
+ headers: {
639
+ "Content-Type": "text/event-stream",
640
+ "Cache-Control": "no-cache",
641
+ Connection: "keep-alive",
642
+ "Access-Control-Allow-Origin": "*"
643
+ }
644
+ });
645
+ }
646
+ send(clientId, message) {
647
+ const client = this.clients.get(clientId);
648
+ if (!client)
649
+ return false;
650
+ try {
651
+ const data = `data: ${JSON.stringify(message)}
652
+
653
+ `;
654
+ client.controller.enqueue(client.encoder.encode(data));
655
+ return true;
656
+ } catch {
657
+ this.removeClient(clientId);
658
+ return false;
659
+ }
660
+ }
661
+ sendEvent(clientId, event, data) {
662
+ const client = this.clients.get(clientId);
663
+ if (!client)
664
+ return false;
665
+ try {
666
+ const message = `event: ${event}
667
+ data: ${JSON.stringify(data)}
668
+
669
+ `;
670
+ client.controller.enqueue(client.encoder.encode(message));
671
+ return true;
672
+ } catch {
673
+ this.removeClient(clientId);
674
+ return false;
675
+ }
676
+ }
677
+ broadcast(message) {
678
+ for (const clientId of this.clients.keys()) {
679
+ this.send(clientId, message);
680
+ }
681
+ }
682
+ removeClient(clientId) {
683
+ const client = this.clients.get(clientId);
684
+ if (client) {
685
+ clearInterval(client.heartbeat);
686
+ this.clients.delete(clientId);
687
+ this.onDisconnectCallback?.(clientId);
688
+ }
689
+ }
690
+ closeClient(clientId) {
691
+ const client = this.clients.get(clientId);
692
+ if (client) {
693
+ try {
694
+ client.controller.close();
695
+ } catch {}
696
+ this.removeClient(clientId);
697
+ }
698
+ }
699
+ getClientCount() {
700
+ return this.clients.size;
701
+ }
702
+ getClientIds() {
703
+ return Array.from(this.clients.keys());
704
+ }
705
+ hasClient(clientId) {
706
+ return this.clients.has(clientId);
707
+ }
708
+ closeAll() {
709
+ for (const clientId of this.clients.keys()) {
710
+ this.closeClient(clientId);
711
+ }
712
+ }
713
+ }
714
+ function createSSEHandler(config = {}) {
715
+ return new SSEHandler(config);
716
+ }
717
+
718
+ // src/handlers/http.ts
719
+ function createHTTPHandler(server, options = {}) {
720
+ const { pathPrefix = "", cors } = options;
721
+ const corsHeaders = {
722
+ "Access-Control-Allow-Origin": cors?.origin ? Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin : "*",
723
+ "Access-Control-Allow-Methods": cors?.methods?.join(", ") ?? "GET, POST, OPTIONS",
724
+ "Access-Control-Allow-Headers": cors?.headers?.join(", ") ?? "Content-Type, Authorization"
725
+ };
726
+ const handler = async (request) => {
727
+ const url = new URL(request.url);
728
+ const pathname = url.pathname;
729
+ if (request.method === "OPTIONS") {
730
+ return new Response(null, {
731
+ status: 204,
732
+ headers: corsHeaders
733
+ });
734
+ }
735
+ const metadataPath = `${pathPrefix}/__lens/metadata`;
736
+ if (request.method === "GET" && pathname === metadataPath) {
737
+ return new Response(JSON.stringify(server.getMetadata()), {
738
+ headers: {
739
+ "Content-Type": "application/json",
740
+ ...corsHeaders
741
+ }
742
+ });
743
+ }
744
+ const operationPath = pathPrefix || "/";
745
+ if (request.method === "POST" && (pathname === operationPath || pathname === `${pathPrefix}/`)) {
746
+ try {
747
+ const body = await request.json();
748
+ const operationPath2 = body.operation ?? body.path;
749
+ if (!operationPath2) {
750
+ return new Response(JSON.stringify({ error: "Missing operation path" }), {
751
+ status: 400,
752
+ headers: {
753
+ "Content-Type": "application/json",
754
+ ...corsHeaders
755
+ }
756
+ });
757
+ }
758
+ const result2 = await server.execute({
759
+ path: operationPath2,
760
+ input: body.input
761
+ });
762
+ if (result2.error) {
763
+ return new Response(JSON.stringify({ error: result2.error.message }), {
764
+ status: 500,
765
+ headers: {
766
+ "Content-Type": "application/json",
767
+ ...corsHeaders
768
+ }
769
+ });
770
+ }
771
+ return new Response(JSON.stringify({ data: result2.data }), {
772
+ headers: {
773
+ "Content-Type": "application/json",
774
+ ...corsHeaders
775
+ }
776
+ });
777
+ } catch (error) {
778
+ return new Response(JSON.stringify({ error: String(error) }), {
779
+ status: 500,
780
+ headers: {
781
+ "Content-Type": "application/json",
782
+ ...corsHeaders
783
+ }
784
+ });
785
+ }
786
+ }
787
+ return new Response(JSON.stringify({ error: "Not found" }), {
788
+ status: 404,
789
+ headers: {
790
+ "Content-Type": "application/json",
791
+ ...corsHeaders
792
+ }
793
+ });
794
+ };
795
+ const result = handler;
796
+ result.handle = handler;
797
+ return result;
798
+ }
799
+
800
+ // src/handlers/unified.ts
801
+ function createHandler(server, options = {}) {
802
+ const { ssePath = "/__lens/sse", heartbeatInterval, ...httpOptions } = options;
803
+ const pathPrefix = httpOptions.pathPrefix ?? "";
804
+ const fullSsePath = `${pathPrefix}${ssePath}`;
805
+ const httpHandler = createHTTPHandler(server, httpOptions);
806
+ const pluginManager = server.getPluginManager();
807
+ const sseHandler = new SSEHandler({
808
+ ...heartbeatInterval !== undefined && { heartbeatInterval },
809
+ onConnect: (client) => {
810
+ pluginManager.runOnConnect({ clientId: client.id });
811
+ },
812
+ onDisconnect: (clientId) => {
813
+ pluginManager.runOnDisconnect({ clientId, subscriptionCount: 0 });
814
+ }
815
+ });
816
+ const handler = async (request) => {
817
+ const url = new URL(request.url);
818
+ if (request.method === "GET" && url.pathname === fullSsePath) {
819
+ return sseHandler.handleConnection(request);
820
+ }
821
+ return httpHandler(request);
822
+ };
823
+ const result = handler;
824
+ result.handle = handler;
825
+ result.sse = sseHandler;
826
+ return result;
827
+ }
828
+ // src/handlers/framework.ts
829
+ function createServerClientProxy(server) {
830
+ function createProxy(path) {
831
+ return new Proxy(() => {}, {
832
+ get(_, prop) {
833
+ if (typeof prop === "symbol")
834
+ return;
835
+ if (prop === "then")
836
+ return;
837
+ const newPath = path ? `${path}.${prop}` : String(prop);
838
+ return createProxy(newPath);
839
+ },
840
+ async apply(_, __, args) {
841
+ const input = args[0];
842
+ const result = await server.execute({ path, input });
843
+ if (result.error) {
844
+ throw result.error;
845
+ }
846
+ return result.data;
847
+ }
848
+ });
849
+ }
850
+ return createProxy("");
851
+ }
852
+ async function handleWebQuery(server, path, url) {
853
+ try {
854
+ const inputParam = url.searchParams.get("input");
855
+ const input = inputParam ? JSON.parse(inputParam) : undefined;
856
+ const result = await server.execute({ path, input });
857
+ if (result.error) {
858
+ return Response.json({ error: result.error.message }, { status: 400 });
859
+ }
860
+ return Response.json({ data: result.data });
861
+ } catch (error) {
862
+ return Response.json({ error: error instanceof Error ? error.message : "Unknown error" }, { status: 500 });
863
+ }
864
+ }
865
+ async function handleWebMutation(server, path, request) {
866
+ try {
867
+ const body = await request.json();
868
+ const input = body.input;
869
+ const result = await server.execute({ path, input });
870
+ if (result.error) {
871
+ return Response.json({ error: result.error.message }, { status: 400 });
872
+ }
873
+ return Response.json({ data: result.data });
874
+ } catch (error) {
875
+ return Response.json({ error: error instanceof Error ? error.message : "Unknown error" }, { status: 500 });
876
+ }
877
+ }
878
+ function handleWebSSE(server, path, url, signal) {
879
+ const inputParam = url.searchParams.get("input");
880
+ const input = inputParam ? JSON.parse(inputParam) : undefined;
881
+ const stream = new ReadableStream({
882
+ start(controller) {
883
+ const encoder = new TextEncoder;
884
+ const result = server.execute({ path, input });
885
+ if (result && typeof result === "object" && "subscribe" in result) {
886
+ const observable = result;
887
+ const subscription = observable.subscribe({
888
+ next: (value) => {
889
+ const data = `data: ${JSON.stringify(value.data)}
890
+
891
+ `;
892
+ controller.enqueue(encoder.encode(data));
893
+ },
894
+ error: (err) => {
895
+ const data = `event: error
896
+ data: ${JSON.stringify({ error: err.message })}
897
+
898
+ `;
899
+ controller.enqueue(encoder.encode(data));
900
+ controller.close();
901
+ },
902
+ complete: () => {
903
+ controller.close();
904
+ }
905
+ });
906
+ if (signal) {
907
+ signal.addEventListener("abort", () => {
908
+ subscription.unsubscribe();
909
+ controller.close();
910
+ });
911
+ }
912
+ }
913
+ }
914
+ });
915
+ return new Response(stream, {
916
+ headers: {
917
+ "Content-Type": "text/event-stream",
918
+ "Cache-Control": "no-cache",
919
+ Connection: "keep-alive"
920
+ }
921
+ });
922
+ }
923
+ function createFrameworkHandler(server, options = {}) {
924
+ const basePath = options.basePath ?? "";
925
+ return async (request) => {
926
+ const url = new URL(request.url);
927
+ const path = url.pathname.replace(basePath, "").replace(/^\//, "");
928
+ if (request.headers.get("accept") === "text/event-stream") {
929
+ return handleWebSSE(server, path, url, request.signal);
930
+ }
931
+ if (request.method === "GET") {
932
+ return handleWebQuery(server, path, url);
933
+ }
934
+ if (request.method === "POST") {
935
+ return handleWebMutation(server, path, request);
936
+ }
937
+ return new Response("Method not allowed", { status: 405 });
938
+ };
939
+ }
940
+ // src/handlers/ws.ts
941
+ function createWSHandler(server, options = {}) {
942
+ const { logger = {} } = options;
943
+ const connections = new Map;
944
+ const wsToConnection = new WeakMap;
945
+ let connectionCounter = 0;
946
+ async function handleConnection(ws) {
947
+ const clientId = `client_${++connectionCounter}`;
948
+ const conn = {
949
+ id: clientId,
950
+ ws,
951
+ subscriptions: new Map
952
+ };
953
+ connections.set(clientId, conn);
954
+ wsToConnection.set(ws, conn);
955
+ const sendFn = (msg) => {
956
+ ws.send(JSON.stringify(msg));
957
+ };
958
+ const allowed = await server.addClient(clientId, sendFn);
959
+ if (!allowed) {
960
+ ws.close();
961
+ connections.delete(clientId);
962
+ return;
963
+ }
718
964
  ws.onmessage = (event) => {
719
- this.handleMessage(conn, event.data);
965
+ handleMessage(conn, event.data);
720
966
  };
721
967
  ws.onclose = () => {
722
- this.handleDisconnect(conn);
968
+ handleDisconnect(conn);
723
969
  };
724
970
  }
725
- handleMessage(conn, data) {
971
+ function handleMessage(conn, data) {
726
972
  try {
727
973
  const message = JSON.parse(data);
728
974
  switch (message.type) {
729
975
  case "handshake":
730
- this.handleHandshake(conn, message);
976
+ handleHandshake(conn, message);
731
977
  break;
732
978
  case "subscribe":
733
- this.handleSubscribe(conn, message);
979
+ handleSubscribe(conn, message);
734
980
  break;
735
981
  case "updateFields":
736
- this.handleUpdateFields(conn, message);
982
+ handleUpdateFields(conn, message);
737
983
  break;
738
984
  case "unsubscribe":
739
- this.handleUnsubscribe(conn, message);
985
+ handleUnsubscribe(conn, message);
740
986
  break;
741
987
  case "query":
742
- this.handleQuery(conn, message);
988
+ handleQuery(conn, message);
743
989
  break;
744
990
  case "mutation":
745
- this.handleMutation(conn, message);
991
+ handleMutation(conn, message);
992
+ break;
993
+ case "reconnect":
994
+ handleReconnect(conn, message);
746
995
  break;
747
996
  }
748
997
  } catch (error) {
@@ -752,165 +1001,125 @@ class LensServerImpl {
752
1001
  }));
753
1002
  }
754
1003
  }
755
- handleHandshake(conn, message) {
1004
+ function handleHandshake(conn, message) {
1005
+ const metadata = server.getMetadata();
756
1006
  conn.ws.send(JSON.stringify({
757
1007
  type: "handshake",
758
1008
  id: message.id,
759
- version: this.version,
760
- operations: this.buildOperationsMap()
1009
+ version: metadata.version,
1010
+ operations: metadata.operations
761
1011
  }));
762
1012
  }
763
- async handleSubscribe(conn, message) {
1013
+ async function handleSubscribe(conn, message) {
764
1014
  const { id, operation, input, fields } = message;
765
- const sub = {
766
- id,
767
- operation,
768
- input,
769
- fields,
770
- entityKeys: new Set,
771
- cleanups: [],
772
- lastData: null
773
- };
774
- conn.subscriptions.set(id, sub);
1015
+ let result;
775
1016
  try {
776
- await this.executeSubscription(conn, sub);
1017
+ result = await server.execute({ path: operation, input });
1018
+ if (result.error) {
1019
+ conn.ws.send(JSON.stringify({
1020
+ type: "error",
1021
+ id,
1022
+ error: { code: "EXECUTION_ERROR", message: result.error.message }
1023
+ }));
1024
+ return;
1025
+ }
777
1026
  } catch (error) {
778
1027
  conn.ws.send(JSON.stringify({
779
1028
  type: "error",
780
1029
  id,
781
1030
  error: { code: "EXECUTION_ERROR", message: String(error) }
782
1031
  }));
1032
+ return;
783
1033
  }
784
- }
785
- async executeSubscription(conn, sub) {
786
- const queryDef = this.queries[sub.operation];
787
- if (!queryDef) {
788
- throw new Error(`Query not found: ${sub.operation}`);
789
- }
790
- if (queryDef._input && sub.input !== undefined) {
791
- const result = queryDef._input.safeParse(sub.input);
792
- if (!result.success) {
793
- throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
794
- }
795
- }
796
- const context = await this.contextFactory();
797
- let isFirstUpdate = true;
798
- const emitData = (data) => {
799
- if (!data)
800
- return;
801
- const entityName = this.getEntityNameFromOutput(queryDef._output);
802
- const entities = this.extractEntities(entityName, data);
803
- for (const { entity, id, entityData } of entities) {
804
- const entityKey = `${entity}:${id}`;
805
- sub.entityKeys.add(entityKey);
806
- this.stateManager.subscribe(conn.id, entity, id, sub.fields);
807
- this.stateManager.emit(entity, id, entityData);
808
- }
809
- if (isFirstUpdate) {
810
- conn.ws.send(JSON.stringify({
811
- type: "data",
812
- id: sub.id,
813
- data
814
- }));
815
- isFirstUpdate = false;
816
- sub.lastData = data;
817
- } else {
818
- const updates = this.computeUpdates(sub.lastData, data);
819
- if (updates && Object.keys(updates).length > 0) {
820
- conn.ws.send(JSON.stringify({
821
- type: "update",
822
- id: sub.id,
823
- updates
824
- }));
1034
+ const entities = result.data ? extractEntities(result.data) : [];
1035
+ const existingSub = conn.subscriptions.get(id);
1036
+ if (existingSub) {
1037
+ for (const cleanup of existingSub.cleanups) {
1038
+ try {
1039
+ cleanup();
1040
+ } catch (e) {
1041
+ logger.error?.("Cleanup error:", e);
825
1042
  }
826
- sub.lastData = data;
827
1043
  }
828
- };
829
- await runWithContext(this.ctx, context, async () => {
830
- const resolver = queryDef._resolve;
831
- if (!resolver) {
832
- throw new Error(`Query ${sub.operation} has no resolver`);
833
- }
834
- const emit = createEmit((command) => {
835
- const entityName = this.getEntityNameFromOutput(queryDef._output);
836
- if (entityName) {
837
- const entities = this.extractEntities(entityName, command.type === "full" ? command.data : {});
838
- for (const { entity, id } of entities) {
839
- this.stateManager.processCommand(entity, id, command);
840
- }
841
- }
842
- if (command.type === "full") {
843
- emitData(command.data);
844
- }
1044
+ server.unsubscribe({
1045
+ clientId: conn.id,
1046
+ subscriptionId: id,
1047
+ operation: existingSub.operation,
1048
+ entityKeys: Array.from(existingSub.entityKeys)
845
1049
  });
846
- const onCleanup = (fn) => {
847
- sub.cleanups.push(fn);
848
- return () => {
849
- const idx = sub.cleanups.indexOf(fn);
850
- if (idx >= 0)
851
- sub.cleanups.splice(idx, 1);
852
- };
853
- };
854
- const lensContext = {
855
- ...context,
856
- emit,
857
- onCleanup
858
- };
859
- const result = resolver({
860
- input: sub.input,
861
- ctx: lensContext
1050
+ conn.subscriptions.delete(id);
1051
+ }
1052
+ const sub = {
1053
+ id,
1054
+ operation,
1055
+ input,
1056
+ fields,
1057
+ entityKeys: new Set(entities.map(({ entity, entityId }) => `${entity}:${entityId}`)),
1058
+ cleanups: [],
1059
+ lastData: result.data
1060
+ };
1061
+ for (const { entity, entityId, entityData } of entities) {
1062
+ const allowed = await server.subscribe({
1063
+ clientId: conn.id,
1064
+ subscriptionId: id,
1065
+ operation,
1066
+ input,
1067
+ fields,
1068
+ entity,
1069
+ entityId
862
1070
  });
863
- if (isAsyncIterable(result)) {
864
- for await (const value of result) {
865
- emitData(value);
866
- }
867
- } else {
868
- const value = await result;
869
- emitData(value);
1071
+ if (!allowed) {
1072
+ conn.ws.send(JSON.stringify({
1073
+ type: "error",
1074
+ id,
1075
+ error: { code: "SUBSCRIPTION_REJECTED", message: "Subscription rejected by plugin" }
1076
+ }));
1077
+ return;
870
1078
  }
871
- });
1079
+ await server.send(conn.id, id, entity, entityId, entityData, true);
1080
+ }
1081
+ conn.subscriptions.set(id, sub);
872
1082
  }
873
- handleUpdateFields(conn, message) {
1083
+ async function handleUpdateFields(conn, message) {
874
1084
  const sub = conn.subscriptions.get(message.id);
875
1085
  if (!sub)
876
1086
  return;
1087
+ const previousFields = sub.fields;
1088
+ let newFields;
877
1089
  if (message.addFields?.includes("*")) {
878
- sub.fields = "*";
879
- for (const entityKey of sub.entityKeys) {
880
- const [entity, id] = entityKey.split(":");
881
- this.stateManager.updateSubscription(conn.id, entity, id, "*");
882
- }
1090
+ newFields = "*";
1091
+ } else if (message.setFields !== undefined) {
1092
+ newFields = message.setFields;
1093
+ } else if (sub.fields === "*") {
883
1094
  return;
884
- }
885
- if (message.setFields !== undefined) {
886
- sub.fields = message.setFields;
887
- for (const entityKey of sub.entityKeys) {
888
- const [entity, id] = entityKey.split(":");
889
- this.stateManager.updateSubscription(conn.id, entity, id, sub.fields);
890
- }
891
- return;
892
- }
893
- if (sub.fields === "*") {
894
- return;
895
- }
896
- const fields = new Set(sub.fields);
897
- if (message.addFields) {
898
- for (const field of message.addFields) {
899
- fields.add(field);
1095
+ } else {
1096
+ const fields = new Set(sub.fields);
1097
+ if (message.addFields) {
1098
+ for (const field of message.addFields) {
1099
+ fields.add(field);
1100
+ }
900
1101
  }
901
- }
902
- if (message.removeFields) {
903
- for (const field of message.removeFields) {
904
- fields.delete(field);
1102
+ if (message.removeFields) {
1103
+ for (const field of message.removeFields) {
1104
+ fields.delete(field);
1105
+ }
905
1106
  }
1107
+ newFields = Array.from(fields);
906
1108
  }
907
- sub.fields = Array.from(fields);
1109
+ sub.fields = newFields;
908
1110
  for (const entityKey of sub.entityKeys) {
909
- const [entity, id] = entityKey.split(":");
910
- this.stateManager.updateSubscription(conn.id, entity, id, sub.fields);
1111
+ const [entity, entityId] = entityKey.split(":");
1112
+ await server.updateFields({
1113
+ clientId: conn.id,
1114
+ subscriptionId: sub.id,
1115
+ entity,
1116
+ entityId,
1117
+ fields: newFields,
1118
+ previousFields
1119
+ });
911
1120
  }
912
1121
  }
913
- handleUnsubscribe(conn, message) {
1122
+ function handleUnsubscribe(conn, message) {
914
1123
  const sub = conn.subscriptions.get(message.id);
915
1124
  if (!sub)
916
1125
  return;
@@ -918,23 +1127,32 @@ class LensServerImpl {
918
1127
  try {
919
1128
  cleanup();
920
1129
  } catch (e) {
921
- this.logger.error?.("Cleanup error:", e);
1130
+ logger.error?.("Cleanup error:", e);
922
1131
  }
923
1132
  }
924
- for (const entityKey of sub.entityKeys) {
925
- const [entity, id] = entityKey.split(":");
926
- this.stateManager.unsubscribe(conn.id, entity, id);
927
- }
928
1133
  conn.subscriptions.delete(message.id);
1134
+ server.unsubscribe({
1135
+ clientId: conn.id,
1136
+ subscriptionId: message.id,
1137
+ operation: sub.operation,
1138
+ entityKeys: Array.from(sub.entityKeys)
1139
+ });
929
1140
  }
930
- async handleQuery(conn, message) {
1141
+ async function handleQuery(conn, message) {
931
1142
  try {
932
- let input = message.input;
933
- if (message.select) {
934
- input = { ...message.input || {}, $select: message.select };
1143
+ const result = await server.execute({
1144
+ path: message.operation,
1145
+ input: message.input
1146
+ });
1147
+ if (result.error) {
1148
+ conn.ws.send(JSON.stringify({
1149
+ type: "error",
1150
+ id: message.id,
1151
+ error: { code: "EXECUTION_ERROR", message: result.error.message }
1152
+ }));
1153
+ return;
935
1154
  }
936
- const result = await this.executeQuery(message.operation, input);
937
- const selected = message.fields && !message.select ? this.applySelection(result, message.fields) : result;
1155
+ const selected = message.fields ? applySelection2(result.data, message.fields) : result.data;
938
1156
  conn.ws.send(JSON.stringify({
939
1157
  type: "result",
940
1158
  id: message.id,
@@ -948,18 +1166,30 @@ class LensServerImpl {
948
1166
  }));
949
1167
  }
950
1168
  }
951
- async handleMutation(conn, message) {
1169
+ async function handleMutation(conn, message) {
952
1170
  try {
953
- const result = await this.executeMutation(message.operation, message.input);
954
- const entityName = this.getEntityNameFromMutation(message.operation);
955
- const entities = this.extractEntities(entityName, result);
956
- for (const { entity, id, entityData } of entities) {
957
- this.stateManager.emit(entity, id, entityData);
1171
+ const result = await server.execute({
1172
+ path: message.operation,
1173
+ input: message.input
1174
+ });
1175
+ if (result.error) {
1176
+ conn.ws.send(JSON.stringify({
1177
+ type: "error",
1178
+ id: message.id,
1179
+ error: { code: "EXECUTION_ERROR", message: result.error.message }
1180
+ }));
1181
+ return;
1182
+ }
1183
+ if (result.data) {
1184
+ const entities = extractEntities(result.data);
1185
+ for (const { entity, entityId, entityData } of entities) {
1186
+ await server.broadcast(entity, entityId, entityData);
1187
+ }
958
1188
  }
959
1189
  conn.ws.send(JSON.stringify({
960
1190
  type: "result",
961
1191
  id: message.id,
962
- data: result
1192
+ data: result.data
963
1193
  }));
964
1194
  } catch (error) {
965
1195
  conn.ws.send(JSON.stringify({
@@ -969,250 +1199,131 @@ class LensServerImpl {
969
1199
  }));
970
1200
  }
971
1201
  }
972
- handleDisconnect(conn) {
973
- for (const sub of conn.subscriptions.values()) {
974
- for (const cleanup of sub.cleanups) {
975
- try {
976
- cleanup();
977
- } catch (e) {
978
- this.logger.error?.("Cleanup error:", e);
979
- }
980
- }
981
- }
982
- this.stateManager.removeClient(conn.id);
983
- this.connections.delete(conn.id);
984
- }
985
- async executeQuery(name, input) {
986
- const queryDef = this.queries[name];
987
- if (!queryDef) {
988
- throw new Error(`Query not found: ${name}`);
989
- }
990
- let select;
991
- let cleanInput = input;
992
- if (input && typeof input === "object" && "$select" in input) {
993
- const { $select, ...rest } = input;
994
- select = $select;
995
- cleanInput = Object.keys(rest).length > 0 ? rest : undefined;
996
- }
997
- if (queryDef._input && cleanInput !== undefined) {
998
- const result = queryDef._input.safeParse(cleanInput);
999
- if (!result.success) {
1000
- throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
1001
- }
1002
- }
1003
- const context = await this.contextFactory();
1004
- try {
1005
- return await runWithContext(this.ctx, context, async () => {
1006
- const resolver = queryDef._resolve;
1007
- if (!resolver) {
1008
- throw new Error(`Query ${name} has no resolver`);
1009
- }
1010
- const emit = createEmit(() => {});
1011
- const onCleanup = () => () => {};
1012
- const lensContext = {
1013
- ...context,
1014
- emit,
1015
- onCleanup
1202
+ async function handleReconnect(conn, message) {
1203
+ const startTime = Date.now();
1204
+ const ctx = {
1205
+ clientId: conn.id,
1206
+ reconnectId: message.reconnectId,
1207
+ subscriptions: message.subscriptions.map((sub) => {
1208
+ const mapped = {
1209
+ id: sub.id,
1210
+ entity: sub.entity,
1211
+ entityId: sub.entityId,
1212
+ fields: sub.fields,
1213
+ version: sub.version
1016
1214
  };
1017
- const result = resolver({
1018
- input: cleanInput,
1019
- ctx: lensContext
1020
- });
1021
- let data;
1022
- if (isAsyncIterable(result)) {
1023
- for await (const value of result) {
1024
- data = value;
1025
- break;
1026
- }
1027
- if (data === undefined) {
1028
- throw new Error(`Query ${name} returned empty stream`);
1029
- }
1030
- } else {
1031
- data = await result;
1215
+ if (sub.dataHash !== undefined) {
1216
+ mapped.dataHash = sub.dataHash;
1032
1217
  }
1033
- return this.processQueryResult(name, data, select);
1034
- });
1035
- } finally {
1036
- this.clearLoaders();
1037
- }
1038
- }
1039
- async executeMutation(name, input) {
1040
- const mutationDef = this.mutations[name];
1041
- if (!mutationDef) {
1042
- throw new Error(`Mutation not found: ${name}`);
1043
- }
1044
- if (mutationDef._input) {
1045
- const result = mutationDef._input.safeParse(input);
1046
- if (!result.success) {
1047
- throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
1048
- }
1049
- }
1050
- const context = await this.contextFactory();
1051
- try {
1052
- return await runWithContext(this.ctx, context, async () => {
1053
- const resolver = mutationDef._resolve;
1054
- if (!resolver) {
1055
- throw new Error(`Mutation ${name} has no resolver`);
1218
+ if (sub.input !== undefined) {
1219
+ mapped.input = sub.input;
1056
1220
  }
1057
- const emit = createEmit(() => {});
1058
- const onCleanup = () => () => {};
1059
- const lensContext = {
1060
- ...context,
1061
- emit,
1062
- onCleanup
1063
- };
1064
- const result = await resolver({
1065
- input,
1066
- ctx: lensContext
1067
- });
1068
- const entityName = this.getEntityNameFromMutation(name);
1069
- const entities = this.extractEntities(entityName, result);
1070
- for (const { entity, id, entityData } of entities) {
1071
- this.stateManager.emit(entity, id, entityData);
1221
+ return mapped;
1222
+ })
1223
+ };
1224
+ const results = await server.handleReconnect(ctx);
1225
+ if (results === null) {
1226
+ conn.ws.send(JSON.stringify({
1227
+ type: "error",
1228
+ error: {
1229
+ code: "RECONNECT_ERROR",
1230
+ message: "State management not available for reconnection",
1231
+ reconnectId: message.reconnectId
1072
1232
  }
1073
- return result;
1074
- });
1075
- } finally {
1076
- this.clearLoaders();
1077
- }
1078
- }
1079
- async handleRequest(req) {
1080
- const url = new URL(req.url);
1081
- if (req.method === "GET" && url.pathname.endsWith("/__lens/metadata")) {
1082
- return new Response(JSON.stringify(this.getMetadata()), {
1083
- headers: { "Content-Type": "application/json" }
1084
- });
1233
+ }));
1234
+ return;
1085
1235
  }
1086
- if (req.method === "POST") {
1087
- try {
1088
- const body = await req.json();
1089
- if (this.queries[body.operation]) {
1090
- const result = await this.executeQuery(body.operation, body.input);
1091
- return new Response(JSON.stringify({ data: result }), {
1092
- headers: { "Content-Type": "application/json" }
1093
- });
1094
- }
1095
- if (this.mutations[body.operation]) {
1096
- const result = await this.executeMutation(body.operation, body.input);
1097
- return new Response(JSON.stringify({ data: result }), {
1098
- headers: { "Content-Type": "application/json" }
1099
- });
1236
+ try {
1237
+ for (const sub of message.subscriptions) {
1238
+ let clientSub = conn.subscriptions.get(sub.id);
1239
+ if (!clientSub) {
1240
+ clientSub = {
1241
+ id: sub.id,
1242
+ operation: "",
1243
+ input: sub.input,
1244
+ fields: sub.fields,
1245
+ entityKeys: new Set([`${sub.entity}:${sub.entityId}`]),
1246
+ cleanups: [],
1247
+ lastData: null
1248
+ };
1249
+ conn.subscriptions.set(sub.id, clientSub);
1100
1250
  }
1101
- return new Response(JSON.stringify({ error: `Operation not found: ${body.operation}` }), {
1102
- status: 404,
1103
- headers: { "Content-Type": "application/json" }
1104
- });
1105
- } catch (error) {
1106
- return new Response(JSON.stringify({ error: String(error) }), {
1107
- status: 500,
1108
- headers: { "Content-Type": "application/json" }
1109
- });
1110
1251
  }
1111
- }
1112
- return new Response("Method not allowed", { status: 405 });
1113
- }
1114
- async listen(port) {
1115
- this.server = Bun.serve({
1116
- port,
1117
- fetch: (req, server) => {
1118
- if (server.upgrade(req)) {
1119
- return;
1120
- }
1121
- return this.handleRequest(req);
1122
- },
1123
- websocket: {
1124
- message: (ws, message) => {
1125
- const conn = this.findConnectionByWs(ws);
1126
- if (conn) {
1127
- this.handleMessage(conn, String(message));
1128
- }
1129
- },
1130
- close: (ws) => {
1131
- const conn = this.findConnectionByWs(ws);
1132
- if (conn) {
1133
- this.handleDisconnect(conn);
1134
- }
1252
+ conn.ws.send(JSON.stringify({
1253
+ type: "reconnect_ack",
1254
+ results,
1255
+ serverTime: Date.now(),
1256
+ reconnectId: message.reconnectId,
1257
+ processingTime: Date.now() - startTime
1258
+ }));
1259
+ } catch (error) {
1260
+ conn.ws.send(JSON.stringify({
1261
+ type: "error",
1262
+ error: {
1263
+ code: "RECONNECT_ERROR",
1264
+ message: String(error),
1265
+ reconnectId: message.reconnectId
1135
1266
  }
1136
- }
1137
- });
1138
- this.logger.info?.(`Lens server listening on port ${port}`);
1139
- }
1140
- async close() {
1141
- if (this.server && typeof this.server.stop === "function") {
1142
- this.server.stop();
1143
- }
1144
- this.server = null;
1145
- }
1146
- findConnectionByWs(ws) {
1147
- for (const conn of this.connections.values()) {
1148
- if (conn.ws === ws) {
1149
- return conn;
1150
- }
1267
+ }));
1151
1268
  }
1152
- return;
1153
1269
  }
1154
- getEntityNameFromOutput(output) {
1155
- if (!output)
1156
- return "unknown";
1157
- if (typeof output === "object" && output !== null) {
1158
- if ("_name" in output) {
1159
- return output._name;
1160
- }
1161
- if ("name" in output) {
1162
- return output.name;
1163
- }
1164
- }
1165
- if (Array.isArray(output) && output.length > 0) {
1166
- const first = output[0];
1167
- if (typeof first === "object" && first !== null) {
1168
- if ("_name" in first) {
1169
- return first._name;
1170
- }
1171
- if ("name" in first) {
1172
- return first.name;
1270
+ function handleDisconnect(conn) {
1271
+ const subscriptionCount = conn.subscriptions.size;
1272
+ for (const sub of conn.subscriptions.values()) {
1273
+ for (const cleanup of sub.cleanups) {
1274
+ try {
1275
+ cleanup();
1276
+ } catch (e) {
1277
+ logger.error?.("Cleanup error:", e);
1173
1278
  }
1174
1279
  }
1175
1280
  }
1176
- return "unknown";
1177
- }
1178
- getEntityNameFromMutation(name) {
1179
- const mutationDef = this.mutations[name];
1180
- if (!mutationDef)
1181
- return "unknown";
1182
- return this.getEntityNameFromOutput(mutationDef._output);
1281
+ connections.delete(conn.id);
1282
+ server.removeClient(conn.id, subscriptionCount);
1183
1283
  }
1184
- extractEntities(entityName, data) {
1284
+ function extractEntities(data) {
1185
1285
  const results = [];
1186
1286
  if (!data)
1187
1287
  return results;
1188
1288
  if (Array.isArray(data)) {
1189
1289
  for (const item of data) {
1190
1290
  if (item && typeof item === "object" && "id" in item) {
1291
+ const entityName = getEntityName(item);
1191
1292
  results.push({
1192
1293
  entity: entityName,
1193
- id: String(item.id),
1294
+ entityId: String(item.id),
1194
1295
  entityData: item
1195
1296
  });
1196
1297
  }
1197
1298
  }
1198
1299
  } else if (typeof data === "object" && "id" in data) {
1300
+ const entityName = getEntityName(data);
1199
1301
  results.push({
1200
1302
  entity: entityName,
1201
- id: String(data.id),
1303
+ entityId: String(data.id),
1202
1304
  entityData: data
1203
1305
  });
1204
1306
  }
1205
1307
  return results;
1206
1308
  }
1207
- applySelection(data, fields) {
1309
+ function getEntityName(data) {
1310
+ if (!data || typeof data !== "object")
1311
+ return "unknown";
1312
+ if ("__typename" in data)
1313
+ return String(data.__typename);
1314
+ if ("_type" in data)
1315
+ return String(data._type);
1316
+ return "unknown";
1317
+ }
1318
+ function applySelection2(data, fields) {
1208
1319
  if (fields === "*" || !data)
1209
1320
  return data;
1210
1321
  if (Array.isArray(data)) {
1211
- return data.map((item) => this.applySelectionToObject(item, fields));
1322
+ return data.map((item) => applySelectionToObject(item, fields));
1212
1323
  }
1213
- return this.applySelectionToObject(data, fields);
1324
+ return applySelectionToObject(data, fields);
1214
1325
  }
1215
- applySelectionToObject(data, fields) {
1326
+ function applySelectionToObject(data, fields) {
1216
1327
  if (!data || typeof data !== "object")
1217
1328
  return null;
1218
1329
  const result = {};
@@ -1220,299 +1331,748 @@ class LensServerImpl {
1220
1331
  if ("id" in obj) {
1221
1332
  result.id = obj.id;
1222
1333
  }
1223
- if (Array.isArray(fields)) {
1224
- for (const field of fields) {
1225
- if (field in obj) {
1226
- result[field] = obj[field];
1227
- }
1228
- }
1229
- return result;
1230
- }
1231
- for (const [key, value] of Object.entries(fields)) {
1232
- if (value === false)
1233
- continue;
1234
- const dataValue = obj[key];
1235
- if (value === true) {
1236
- result[key] = dataValue;
1237
- } else if (typeof value === "object" && value !== null) {
1238
- const nestedSelect = value.select ?? value;
1239
- if (Array.isArray(dataValue)) {
1240
- result[key] = dataValue.map((item) => this.applySelectionToObject(item, nestedSelect));
1241
- } else if (dataValue !== null && typeof dataValue === "object") {
1242
- result[key] = this.applySelectionToObject(dataValue, nestedSelect);
1243
- } else {
1244
- result[key] = dataValue;
1245
- }
1334
+ for (const field of fields) {
1335
+ if (field in obj) {
1336
+ result[field] = obj[field];
1246
1337
  }
1247
1338
  }
1248
1339
  return result;
1249
1340
  }
1250
- async executeEntityResolvers(entityName, data, select) {
1251
- if (!data || !select || !this.resolverMap)
1252
- return data;
1253
- const resolverDef = this.resolverMap.get(entityName);
1254
- if (!resolverDef)
1255
- return data;
1256
- const result = { ...data };
1257
- const context = await this.contextFactory();
1258
- for (const [fieldName, fieldSelect] of Object.entries(select)) {
1259
- if (fieldSelect === false || fieldSelect === true)
1260
- continue;
1261
- if (!resolverDef.hasField(fieldName))
1262
- continue;
1263
- const fieldArgs = typeof fieldSelect === "object" && fieldSelect !== null && "args" in fieldSelect ? fieldSelect.args ?? {} : {};
1264
- result[fieldName] = await resolverDef.resolveField(fieldName, data, fieldArgs, context);
1265
- const nestedSelect = fieldSelect.select;
1266
- if (nestedSelect && result[fieldName]) {
1267
- const relationData = result[fieldName];
1268
- const targetEntity = this.getRelationTargetEntity(entityName, fieldName);
1269
- if (Array.isArray(relationData)) {
1270
- result[fieldName] = await Promise.all(relationData.map((item) => this.executeEntityResolvers(targetEntity, item, nestedSelect)));
1271
- } else {
1272
- result[fieldName] = await this.executeEntityResolvers(targetEntity, relationData, nestedSelect);
1341
+ const handler = {
1342
+ handleConnection,
1343
+ handler: {
1344
+ open(ws) {
1345
+ if (ws && typeof ws === "object" && "send" in ws) {
1346
+ handleConnection(ws);
1347
+ }
1348
+ },
1349
+ message(ws, message) {
1350
+ const conn = wsToConnection.get(ws);
1351
+ if (conn) {
1352
+ handleMessage(conn, String(message));
1353
+ } else if (ws && typeof ws === "object" && "send" in ws) {
1354
+ handleConnection(ws);
1355
+ const newConn = wsToConnection.get(ws);
1356
+ if (newConn) {
1357
+ handleMessage(newConn, String(message));
1358
+ }
1359
+ }
1360
+ },
1361
+ close(ws) {
1362
+ const conn = wsToConnection.get(ws);
1363
+ if (conn) {
1364
+ handleDisconnect(conn);
1273
1365
  }
1274
1366
  }
1367
+ },
1368
+ async close() {
1369
+ for (const conn of connections.values()) {
1370
+ handleDisconnect(conn);
1371
+ conn.ws.close();
1372
+ }
1373
+ connections.clear();
1374
+ }
1375
+ };
1376
+ return handler;
1377
+ }
1378
+ // src/storage/types.ts
1379
+ var DEFAULT_STORAGE_CONFIG = {
1380
+ maxPatchesPerEntity: 1000,
1381
+ maxPatchAge: 5 * 60 * 1000,
1382
+ cleanupInterval: 60 * 1000,
1383
+ maxRetries: 3
1384
+ };
1385
+
1386
+ // src/storage/memory.ts
1387
+ function makeKey(entity, entityId) {
1388
+ return `${entity}:${entityId}`;
1389
+ }
1390
+ function computePatch(oldState, newState) {
1391
+ const patch = [];
1392
+ const oldKeys = new Set(Object.keys(oldState));
1393
+ const newKeys = new Set(Object.keys(newState));
1394
+ for (const key of newKeys) {
1395
+ const oldValue = oldState[key];
1396
+ const newValue = newState[key];
1397
+ if (!oldKeys.has(key)) {
1398
+ patch.push({ op: "add", path: `/${key}`, value: newValue });
1399
+ } else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
1400
+ patch.push({ op: "replace", path: `/${key}`, value: newValue });
1275
1401
  }
1276
- return result;
1277
1402
  }
1278
- getRelationTargetEntity(entityName, fieldName) {
1279
- const entityDef = this.entities[entityName];
1280
- if (!entityDef)
1281
- return fieldName;
1282
- const fields = entityDef.fields;
1283
- if (!fields)
1284
- return fieldName;
1285
- const fieldDef = fields[fieldName];
1286
- if (!fieldDef)
1287
- return fieldName;
1288
- if (fieldDef._type === "hasMany" || fieldDef._type === "hasOne" || fieldDef._type === "belongsTo") {
1289
- return fieldDef._target ?? fieldName;
1290
- }
1291
- return fieldName;
1292
- }
1293
- serializeEntity(entityName, data) {
1294
- if (data === null)
1295
- return null;
1296
- const entityDef = this.entities[entityName];
1297
- if (!entityDef)
1298
- return data;
1299
- const fields = entityDef.fields;
1300
- if (!fields)
1301
- return data;
1302
- const result = {};
1303
- for (const [fieldName, value] of Object.entries(data)) {
1304
- const fieldType = fields[fieldName];
1305
- if (!fieldType) {
1306
- result[fieldName] = value;
1307
- continue;
1403
+ for (const key of oldKeys) {
1404
+ if (!newKeys.has(key)) {
1405
+ patch.push({ op: "remove", path: `/${key}` });
1406
+ }
1407
+ }
1408
+ return patch;
1409
+ }
1410
+ function hashState(state) {
1411
+ return JSON.stringify(state);
1412
+ }
1413
+ function memoryStorage(config = {}) {
1414
+ const cfg = { ...DEFAULT_STORAGE_CONFIG, ...config };
1415
+ const entities = new Map;
1416
+ let cleanupTimer = null;
1417
+ if (cfg.cleanupInterval > 0) {
1418
+ cleanupTimer = setInterval(() => cleanup(), cfg.cleanupInterval);
1419
+ }
1420
+ function cleanup() {
1421
+ const now = Date.now();
1422
+ const minTimestamp = now - cfg.maxPatchAge;
1423
+ for (const state of entities.values()) {
1424
+ state.patches = state.patches.filter((p) => p.timestamp >= minTimestamp);
1425
+ }
1426
+ }
1427
+ function trimPatches(state) {
1428
+ if (state.patches.length > cfg.maxPatchesPerEntity) {
1429
+ state.patches = state.patches.slice(-cfg.maxPatchesPerEntity);
1430
+ }
1431
+ }
1432
+ return {
1433
+ async emit(entity, entityId, data) {
1434
+ const key = makeKey(entity, entityId);
1435
+ const existing = entities.get(key);
1436
+ const now = Date.now();
1437
+ if (!existing) {
1438
+ const newState = {
1439
+ data: { ...data },
1440
+ version: 1,
1441
+ patches: []
1442
+ };
1443
+ entities.set(key, newState);
1444
+ return {
1445
+ version: 1,
1446
+ patch: null,
1447
+ changed: true
1448
+ };
1308
1449
  }
1309
- if (value === null || value === undefined) {
1310
- result[fieldName] = value;
1311
- continue;
1450
+ const oldHash = hashState(existing.data);
1451
+ const newHash = hashState(data);
1452
+ if (oldHash === newHash) {
1453
+ return {
1454
+ version: existing.version,
1455
+ patch: null,
1456
+ changed: false
1457
+ };
1312
1458
  }
1313
- if (fieldType._type === "hasMany" || fieldType._type === "belongsTo" || fieldType._type === "hasOne") {
1314
- const targetEntity = fieldType._target;
1315
- if (targetEntity && Array.isArray(value)) {
1316
- result[fieldName] = value.map((item) => this.serializeEntity(targetEntity, item));
1317
- } else if (targetEntity && typeof value === "object") {
1318
- result[fieldName] = this.serializeEntity(targetEntity, value);
1319
- } else {
1320
- result[fieldName] = value;
1459
+ const patch = computePatch(existing.data, data);
1460
+ const newVersion = existing.version + 1;
1461
+ existing.data = { ...data };
1462
+ existing.version = newVersion;
1463
+ if (patch.length > 0) {
1464
+ existing.patches.push({
1465
+ version: newVersion,
1466
+ patch,
1467
+ timestamp: now
1468
+ });
1469
+ trimPatches(existing);
1470
+ }
1471
+ return {
1472
+ version: newVersion,
1473
+ patch: patch.length > 0 ? patch : null,
1474
+ changed: true
1475
+ };
1476
+ },
1477
+ async getState(entity, entityId) {
1478
+ const key = makeKey(entity, entityId);
1479
+ const state = entities.get(key);
1480
+ return state ? { ...state.data } : null;
1481
+ },
1482
+ async getVersion(entity, entityId) {
1483
+ const key = makeKey(entity, entityId);
1484
+ const state = entities.get(key);
1485
+ return state?.version ?? 0;
1486
+ },
1487
+ async getLatestPatch(entity, entityId) {
1488
+ const key = makeKey(entity, entityId);
1489
+ const state = entities.get(key);
1490
+ if (!state || state.patches.length === 0) {
1491
+ return null;
1492
+ }
1493
+ return state.patches[state.patches.length - 1].patch;
1494
+ },
1495
+ async getPatchesSince(entity, entityId, sinceVersion) {
1496
+ const key = makeKey(entity, entityId);
1497
+ const state = entities.get(key);
1498
+ if (!state) {
1499
+ return sinceVersion === 0 ? [] : null;
1500
+ }
1501
+ if (sinceVersion >= state.version) {
1502
+ return [];
1503
+ }
1504
+ const relevantPatches = state.patches.filter((p) => p.version > sinceVersion);
1505
+ if (relevantPatches.length === 0) {
1506
+ return null;
1507
+ }
1508
+ relevantPatches.sort((a, b) => a.version - b.version);
1509
+ if (relevantPatches[0].version !== sinceVersion + 1) {
1510
+ return null;
1511
+ }
1512
+ for (let i = 1;i < relevantPatches.length; i++) {
1513
+ if (relevantPatches[i].version !== relevantPatches[i - 1].version + 1) {
1514
+ return null;
1321
1515
  }
1322
- continue;
1323
1516
  }
1324
- if (typeof fieldType.serialize === "function") {
1325
- try {
1326
- result[fieldName] = fieldType.serialize(value);
1327
- } catch (error) {
1328
- this.logger.warn?.(`Failed to serialize field ${entityName}.${fieldName}:`, error);
1329
- result[fieldName] = value;
1517
+ return relevantPatches.map((p) => p.patch);
1518
+ },
1519
+ async has(entity, entityId) {
1520
+ const key = makeKey(entity, entityId);
1521
+ return entities.has(key);
1522
+ },
1523
+ async delete(entity, entityId) {
1524
+ const key = makeKey(entity, entityId);
1525
+ entities.delete(key);
1526
+ },
1527
+ async clear() {
1528
+ entities.clear();
1529
+ },
1530
+ async dispose() {
1531
+ if (cleanupTimer) {
1532
+ clearInterval(cleanupTimer);
1533
+ cleanupTimer = null;
1534
+ }
1535
+ entities.clear();
1536
+ }
1537
+ };
1538
+ }
1539
+ // src/plugin/op-log.ts
1540
+ function opLog(options = {}) {
1541
+ const storage = options.storage ?? memoryStorage(options);
1542
+ const debug = options.debug ?? false;
1543
+ const log = (...args) => {
1544
+ if (debug) {
1545
+ console.log("[opLog]", ...args);
1546
+ }
1547
+ };
1548
+ return {
1549
+ name: "opLog",
1550
+ getStorage() {
1551
+ return storage;
1552
+ },
1553
+ async getVersion(entity, entityId) {
1554
+ return storage.getVersion(entity, entityId);
1555
+ },
1556
+ async getState(entity, entityId) {
1557
+ return storage.getState(entity, entityId);
1558
+ },
1559
+ async getLatestPatch(entity, entityId) {
1560
+ return storage.getLatestPatch(entity, entityId);
1561
+ },
1562
+ async onBroadcast(ctx) {
1563
+ const { entity, entityId, data } = ctx;
1564
+ log("onBroadcast:", entity, entityId);
1565
+ const result = await storage.emit(entity, entityId, data);
1566
+ log(" Version:", result.version, "Patch ops:", result.patch?.length ?? 0);
1567
+ return {
1568
+ version: result.version,
1569
+ patch: result.patch,
1570
+ data
1571
+ };
1572
+ },
1573
+ async onReconnect(ctx) {
1574
+ log("Reconnect:", ctx.clientId, "subscriptions:", ctx.subscriptions.length);
1575
+ const results = [];
1576
+ for (const sub of ctx.subscriptions) {
1577
+ const currentVersion = await storage.getVersion(sub.entity, sub.entityId);
1578
+ const currentState = await storage.getState(sub.entity, sub.entityId);
1579
+ if (currentState === null) {
1580
+ results.push({
1581
+ id: sub.id,
1582
+ entity: sub.entity,
1583
+ entityId: sub.entityId,
1584
+ status: "deleted",
1585
+ version: 0
1586
+ });
1587
+ log(" Subscription", sub.id, `${sub.entity}:${sub.entityId}`, "status: deleted");
1588
+ continue;
1330
1589
  }
1331
- } else {
1332
- result[fieldName] = value;
1590
+ if (sub.version >= currentVersion) {
1591
+ results.push({
1592
+ id: sub.id,
1593
+ entity: sub.entity,
1594
+ entityId: sub.entityId,
1595
+ status: "current",
1596
+ version: currentVersion
1597
+ });
1598
+ log(" Subscription", sub.id, `${sub.entity}:${sub.entityId}`, "status: current", "version:", currentVersion);
1599
+ continue;
1600
+ }
1601
+ const patches = await storage.getPatchesSince(sub.entity, sub.entityId, sub.version);
1602
+ if (patches !== null && patches.length > 0) {
1603
+ results.push({
1604
+ id: sub.id,
1605
+ entity: sub.entity,
1606
+ entityId: sub.entityId,
1607
+ status: "patched",
1608
+ version: currentVersion,
1609
+ patches
1610
+ });
1611
+ log(" Subscription", sub.id, `${sub.entity}:${sub.entityId}`, "status: patched", "version:", currentVersion, "patches:", patches.length);
1612
+ continue;
1613
+ }
1614
+ results.push({
1615
+ id: sub.id,
1616
+ entity: sub.entity,
1617
+ entityId: sub.entityId,
1618
+ status: "snapshot",
1619
+ version: currentVersion,
1620
+ data: currentState
1621
+ });
1622
+ log(" Subscription", sub.id, `${sub.entity}:${sub.entityId}`, "status: snapshot", "version:", currentVersion);
1333
1623
  }
1624
+ return results;
1334
1625
  }
1335
- return result;
1626
+ };
1627
+ }
1628
+ function isOpLogPlugin(plugin) {
1629
+ return plugin.name === "opLog" && "getStorage" in plugin;
1630
+ }
1631
+ // src/plugin/optimistic.ts
1632
+ import {
1633
+ isPipeline,
1634
+ OPTIMISTIC_PLUGIN_SYMBOL
1635
+ } from "@sylphx/lens-core";
1636
+ import { isOptimisticPlugin } from "@sylphx/lens-core";
1637
+ function getEntityTypeName(returnSpec) {
1638
+ if (!returnSpec)
1639
+ return;
1640
+ if (typeof returnSpec !== "object")
1641
+ return;
1642
+ const spec = returnSpec;
1643
+ if ("_name" in spec && typeof spec._name === "string") {
1644
+ return spec._name;
1336
1645
  }
1337
- async processQueryResult(queryName, data, select) {
1338
- if (data === null || data === undefined)
1339
- return data;
1340
- const queryDef = this.queries[queryName];
1341
- const entityName = this.getEntityNameFromOutput(queryDef?._output);
1342
- if (Array.isArray(data)) {
1343
- const processedItems = await Promise.all(data.map(async (item) => {
1344
- let result2 = item;
1345
- if (select && this.resolverMap) {
1346
- result2 = await this.executeEntityResolvers(entityName, item, select);
1646
+ if ("~entity" in spec) {
1647
+ const entity = spec["~entity"];
1648
+ if (entity?.name)
1649
+ return entity.name;
1650
+ }
1651
+ if ("_tag" in spec) {
1652
+ if (spec._tag === "entity" && spec.entityDef) {
1653
+ const entityDef = spec.entityDef;
1654
+ if (entityDef._name)
1655
+ return entityDef._name;
1656
+ }
1657
+ if (spec._tag === "array" && spec.element) {
1658
+ return getEntityTypeName(spec.element);
1659
+ }
1660
+ }
1661
+ return;
1662
+ }
1663
+ function getInputFields(schema) {
1664
+ if (!schema?.shape)
1665
+ return [];
1666
+ return Object.keys(schema.shape);
1667
+ }
1668
+ function $input(field) {
1669
+ return { $input: field };
1670
+ }
1671
+ function sugarToPipeline(sugar, entityType, inputFields) {
1672
+ if (!sugar)
1673
+ return;
1674
+ if (isPipeline(sugar))
1675
+ return sugar;
1676
+ const entity = entityType ?? "Entity";
1677
+ switch (sugar) {
1678
+ case "merge": {
1679
+ const updateData = {
1680
+ type: entity,
1681
+ id: $input("id")
1682
+ };
1683
+ for (const field of inputFields) {
1684
+ if (field !== "id") {
1685
+ updateData[field] = $input(field);
1686
+ }
1687
+ }
1688
+ const pipeline = {
1689
+ $pipe: [
1690
+ {
1691
+ $do: "entity.update",
1692
+ $with: updateData,
1693
+ $as: "result"
1694
+ }
1695
+ ]
1696
+ };
1697
+ return pipeline;
1698
+ }
1699
+ case "create": {
1700
+ const pipeline = {
1701
+ $pipe: [
1702
+ {
1703
+ $do: "entity.create",
1704
+ $with: {
1705
+ type: entity,
1706
+ id: { $temp: true },
1707
+ $fromOutput: true
1708
+ },
1709
+ $as: "result"
1710
+ }
1711
+ ]
1712
+ };
1713
+ return pipeline;
1714
+ }
1715
+ case "delete": {
1716
+ const pipeline = {
1717
+ $pipe: [
1718
+ {
1719
+ $do: "entity.delete",
1720
+ $with: {
1721
+ type: entity,
1722
+ id: { id: $input("id") }
1723
+ },
1724
+ $as: "result"
1725
+ }
1726
+ ]
1727
+ };
1728
+ return pipeline;
1729
+ }
1730
+ default:
1731
+ if (typeof sugar === "object" && "merge" in sugar) {
1732
+ const updateData = {
1733
+ type: entity,
1734
+ id: $input("id")
1735
+ };
1736
+ for (const field of inputFields) {
1737
+ if (field !== "id") {
1738
+ updateData[field] = $input(field);
1739
+ }
1347
1740
  }
1348
- if (select) {
1349
- result2 = this.applySelection(result2, select);
1741
+ for (const [key, value] of Object.entries(sugar.merge)) {
1742
+ updateData[key] = value;
1350
1743
  }
1351
- if (entityName) {
1352
- return this.serializeEntity(entityName, result2);
1744
+ const pipeline = {
1745
+ $pipe: [
1746
+ {
1747
+ $do: "entity.update",
1748
+ $with: updateData,
1749
+ $as: "result"
1750
+ }
1751
+ ]
1752
+ };
1753
+ return pipeline;
1754
+ }
1755
+ return;
1756
+ }
1757
+ }
1758
+ function isOptimisticDSL(value) {
1759
+ if (value === "merge" || value === "create" || value === "delete")
1760
+ return true;
1761
+ if (isPipeline(value))
1762
+ return true;
1763
+ if (typeof value === "object" && value !== null && "merge" in value)
1764
+ return true;
1765
+ return false;
1766
+ }
1767
+ function optimisticPlugin(options = {}) {
1768
+ const { autoDerive = true, debug = false } = options;
1769
+ const log = (...args) => {
1770
+ if (debug) {
1771
+ console.log("[optimisticPlugin]", ...args);
1772
+ }
1773
+ };
1774
+ return {
1775
+ name: "optimistic",
1776
+ [OPTIMISTIC_PLUGIN_SYMBOL]: true,
1777
+ enhanceOperationMeta(ctx) {
1778
+ if (ctx.type !== "mutation")
1779
+ return;
1780
+ const def = ctx.definition;
1781
+ let optimisticSpec = def._optimistic;
1782
+ if (!optimisticSpec && autoDerive) {
1783
+ const lastSegment = ctx.path.includes(".") ? ctx.path.split(".").pop() : ctx.path;
1784
+ if (lastSegment.startsWith("update")) {
1785
+ optimisticSpec = "merge";
1786
+ } else if (lastSegment.startsWith("create") || lastSegment.startsWith("add")) {
1787
+ optimisticSpec = "create";
1788
+ } else if (lastSegment.startsWith("delete") || lastSegment.startsWith("remove")) {
1789
+ optimisticSpec = "delete";
1353
1790
  }
1354
- return result2;
1355
- }));
1356
- return processedItems;
1357
- }
1358
- let result = data;
1359
- if (select && this.resolverMap) {
1360
- result = await this.executeEntityResolvers(entityName, data, select);
1791
+ log(`Auto-derived optimistic for ${ctx.path}:`, optimisticSpec);
1792
+ }
1793
+ if (optimisticSpec && isOptimisticDSL(optimisticSpec)) {
1794
+ const entityType = getEntityTypeName(def._output);
1795
+ const inputFields = getInputFields(def._input);
1796
+ const pipeline = sugarToPipeline(optimisticSpec, entityType, inputFields);
1797
+ if (pipeline) {
1798
+ ctx.meta.optimistic = pipeline;
1799
+ log(`Added optimistic config for ${ctx.path}:`, pipeline);
1800
+ }
1801
+ }
1361
1802
  }
1362
- if (select) {
1363
- result = this.applySelection(result, select);
1803
+ };
1804
+ }
1805
+ // src/reconnect/operation-log.ts
1806
+ import { DEFAULT_OPERATION_LOG_CONFIG } from "@sylphx/lens-core";
1807
+
1808
+ class OperationLog {
1809
+ entries = [];
1810
+ config;
1811
+ totalMemory = 0;
1812
+ entityIndex = new Map;
1813
+ oldestVersionIndex = new Map;
1814
+ newestVersionIndex = new Map;
1815
+ cleanupTimer = null;
1816
+ constructor(config = {}) {
1817
+ this.config = { ...DEFAULT_OPERATION_LOG_CONFIG, ...config };
1818
+ if (this.config.cleanupInterval > 0) {
1819
+ this.cleanupTimer = setInterval(() => this.cleanup(), this.config.cleanupInterval);
1820
+ }
1821
+ }
1822
+ append(entry) {
1823
+ const index = this.entries.length;
1824
+ this.entries.push(entry);
1825
+ this.totalMemory += entry.patchSize;
1826
+ let indices = this.entityIndex.get(entry.entityKey);
1827
+ if (!indices) {
1828
+ indices = [];
1829
+ this.entityIndex.set(entry.entityKey, indices);
1830
+ this.oldestVersionIndex.set(entry.entityKey, entry.version);
1831
+ this.newestVersionIndex.set(entry.entityKey, entry.version);
1832
+ } else {
1833
+ const currentOldest = this.oldestVersionIndex.get(entry.entityKey);
1834
+ const currentNewest = this.newestVersionIndex.get(entry.entityKey);
1835
+ if (entry.version < currentOldest) {
1836
+ this.oldestVersionIndex.set(entry.entityKey, entry.version);
1837
+ }
1838
+ if (entry.version > currentNewest) {
1839
+ this.newestVersionIndex.set(entry.entityKey, entry.version);
1840
+ }
1364
1841
  }
1365
- if (entityName && typeof result === "object" && result !== null) {
1366
- return this.serializeEntity(entityName, result);
1842
+ indices.push(index);
1843
+ this.checkLimits();
1844
+ }
1845
+ appendBatch(entries) {
1846
+ for (const entry of entries) {
1847
+ this.append(entry);
1367
1848
  }
1368
- return result;
1369
1849
  }
1370
- computeUpdates(oldData, newData) {
1371
- if (!oldData || !newData)
1372
- return null;
1373
- if (typeof oldData !== "object" || typeof newData !== "object")
1850
+ getSince(entityKey, fromVersion) {
1851
+ const oldestVersion = this.oldestVersionIndex.get(entityKey);
1852
+ const newestVersion = this.newestVersionIndex.get(entityKey);
1853
+ if (oldestVersion === undefined || newestVersion === undefined) {
1854
+ return fromVersion === 0 ? [] : null;
1855
+ }
1856
+ if (fromVersion >= newestVersion) {
1857
+ return [];
1858
+ }
1859
+ if (fromVersion < oldestVersion - 1) {
1374
1860
  return null;
1375
- const updates = {};
1376
- const oldObj = oldData;
1377
- const newObj = newData;
1378
- for (const key of Object.keys(newObj)) {
1379
- const oldValue = oldObj[key];
1380
- const newValue = newObj[key];
1381
- if (!this.deepEqual(oldValue, newValue)) {
1382
- updates[key] = createUpdate2(oldValue, newValue);
1861
+ }
1862
+ const indices = this.entityIndex.get(entityKey) ?? [];
1863
+ const result = [];
1864
+ for (const idx of indices) {
1865
+ const entry = this.entries[idx];
1866
+ if (entry && entry.version > fromVersion) {
1867
+ result.push(entry);
1868
+ }
1869
+ }
1870
+ result.sort((a, b) => a.version - b.version);
1871
+ if (result.length > 0) {
1872
+ if (result[0].version !== fromVersion + 1) {
1873
+ return null;
1874
+ }
1875
+ for (let i = 1;i < result.length; i++) {
1876
+ if (result[i].version !== result[i - 1].version + 1) {
1877
+ return null;
1878
+ }
1383
1879
  }
1384
1880
  }
1385
- return Object.keys(updates).length > 0 ? updates : null;
1881
+ return result;
1386
1882
  }
1387
- deepEqual(a, b) {
1388
- if (a === b)
1389
- return true;
1390
- if (typeof a !== typeof b)
1391
- return false;
1392
- if (typeof a !== "object" || a === null || b === null)
1883
+ hasVersion(entityKey, version) {
1884
+ const oldest = this.oldestVersionIndex.get(entityKey);
1885
+ const newest = this.newestVersionIndex.get(entityKey);
1886
+ if (oldest === undefined || newest === undefined) {
1393
1887
  return false;
1394
- const aObj = a;
1395
- const bObj = b;
1396
- const aKeys = Object.keys(aObj);
1397
- const bKeys = Object.keys(bObj);
1398
- if (aKeys.length !== bKeys.length)
1399
- return false;
1400
- for (const key of aKeys) {
1401
- if (!this.deepEqual(aObj[key], bObj[key]))
1402
- return false;
1403
1888
  }
1404
- return true;
1889
+ return version >= oldest && version <= newest;
1405
1890
  }
1406
- clearLoaders() {
1407
- for (const loader of this.loaders.values()) {
1408
- loader.clear();
1409
- }
1410
- this.loaders.clear();
1891
+ getOldestVersion(entityKey) {
1892
+ return this.oldestVersionIndex.get(entityKey) ?? null;
1411
1893
  }
1412
- }
1413
- function isAsyncIterable(value) {
1414
- return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
1415
- }
1416
- function createServer(config) {
1417
- const server = new LensServerImpl(config);
1418
- return server;
1419
- }
1420
- // src/sse/handler.ts
1421
- class SSEHandler {
1422
- stateManager;
1423
- heartbeatInterval;
1424
- clients = new Map;
1425
- clientCounter = 0;
1426
- constructor(config) {
1427
- this.stateManager = config.stateManager;
1428
- this.heartbeatInterval = config.heartbeatInterval ?? 30000;
1894
+ getNewestVersion(entityKey) {
1895
+ return this.newestVersionIndex.get(entityKey) ?? null;
1429
1896
  }
1430
- handleConnection(_req) {
1431
- const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
1432
- const encoder = new TextEncoder;
1433
- const stream = new ReadableStream({
1434
- start: (controller) => {
1435
- const stateClient = {
1436
- id: clientId,
1437
- send: (msg) => {
1438
- try {
1439
- const data = `data: ${JSON.stringify(msg)}
1440
-
1441
- `;
1442
- controller.enqueue(encoder.encode(data));
1443
- } catch {
1444
- this.removeClient(clientId);
1445
- }
1446
- }
1447
- };
1448
- this.stateManager.addClient(stateClient);
1449
- controller.enqueue(encoder.encode(`event: connected
1450
- data: ${JSON.stringify({ clientId })}
1451
-
1452
- `));
1453
- const heartbeat = setInterval(() => {
1454
- try {
1455
- controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}
1456
-
1457
- `));
1458
- } catch {
1459
- this.removeClient(clientId);
1460
- }
1461
- }, this.heartbeatInterval);
1462
- this.clients.set(clientId, { controller, heartbeat });
1463
- },
1464
- cancel: () => {
1465
- this.removeClient(clientId);
1897
+ getAll(entityKey) {
1898
+ const indices = this.entityIndex.get(entityKey) ?? [];
1899
+ const result = [];
1900
+ for (const idx of indices) {
1901
+ const entry = this.entries[idx];
1902
+ if (entry) {
1903
+ result.push(entry);
1466
1904
  }
1467
- });
1468
- return new Response(stream, {
1469
- headers: {
1470
- "Content-Type": "text/event-stream",
1471
- "Cache-Control": "no-cache",
1472
- Connection: "keep-alive",
1473
- "Access-Control-Allow-Origin": "*"
1905
+ }
1906
+ return result.sort((a, b) => a.version - b.version);
1907
+ }
1908
+ cleanup() {
1909
+ const now = Date.now();
1910
+ let removedCount = 0;
1911
+ const minTimestamp = now - this.config.maxAge;
1912
+ while (this.entries.length > 0 && this.entries[0].timestamp < minTimestamp) {
1913
+ this.removeOldest();
1914
+ removedCount++;
1915
+ }
1916
+ while (this.entries.length > this.config.maxEntries) {
1917
+ this.removeOldest();
1918
+ removedCount++;
1919
+ }
1920
+ while (this.totalMemory > this.config.maxMemory && this.entries.length > 0) {
1921
+ this.removeOldest();
1922
+ removedCount++;
1923
+ }
1924
+ if (removedCount > this.entries.length * 0.1) {
1925
+ this.rebuildIndices();
1926
+ }
1927
+ }
1928
+ removeOldest() {
1929
+ const removed = this.entries.shift();
1930
+ if (!removed)
1931
+ return;
1932
+ this.totalMemory -= removed.patchSize;
1933
+ const indices = this.entityIndex.get(removed.entityKey);
1934
+ if (indices && indices.length > 0) {
1935
+ indices.shift();
1936
+ if (indices.length === 0) {
1937
+ this.entityIndex.delete(removed.entityKey);
1938
+ this.oldestVersionIndex.delete(removed.entityKey);
1939
+ this.newestVersionIndex.delete(removed.entityKey);
1940
+ } else {
1941
+ const nextEntry = this.entries[indices[0] - 1];
1942
+ if (nextEntry) {
1943
+ this.oldestVersionIndex.set(removed.entityKey, nextEntry.version);
1944
+ }
1474
1945
  }
1475
- });
1946
+ }
1476
1947
  }
1477
- removeClient(clientId) {
1478
- const client = this.clients.get(clientId);
1479
- if (client) {
1480
- clearInterval(client.heartbeat);
1481
- this.clients.delete(clientId);
1948
+ rebuildIndices() {
1949
+ this.entityIndex.clear();
1950
+ this.oldestVersionIndex.clear();
1951
+ this.newestVersionIndex.clear();
1952
+ this.totalMemory = 0;
1953
+ for (let i = 0;i < this.entries.length; i++) {
1954
+ const entry = this.entries[i];
1955
+ this.totalMemory += entry.patchSize;
1956
+ let indices = this.entityIndex.get(entry.entityKey);
1957
+ if (!indices) {
1958
+ indices = [];
1959
+ this.entityIndex.set(entry.entityKey, indices);
1960
+ this.oldestVersionIndex.set(entry.entityKey, entry.version);
1961
+ }
1962
+ indices.push(i);
1963
+ this.newestVersionIndex.set(entry.entityKey, entry.version);
1482
1964
  }
1483
- this.stateManager.removeClient(clientId);
1484
1965
  }
1485
- closeClient(clientId) {
1486
- const client = this.clients.get(clientId);
1487
- if (client) {
1488
- try {
1489
- client.controller.close();
1490
- } catch {}
1491
- this.removeClient(clientId);
1966
+ checkLimits() {
1967
+ const needsCleanup = this.entries.length > this.config.maxEntries || this.totalMemory > this.config.maxMemory;
1968
+ if (needsCleanup) {
1969
+ this.cleanup();
1492
1970
  }
1493
1971
  }
1494
- getClientCount() {
1495
- return this.clients.size;
1972
+ getStats() {
1973
+ return {
1974
+ entryCount: this.entries.length,
1975
+ entityCount: this.entityIndex.size,
1976
+ memoryUsage: this.totalMemory,
1977
+ oldestTimestamp: this.entries[0]?.timestamp ?? null,
1978
+ newestTimestamp: this.entries[this.entries.length - 1]?.timestamp ?? null,
1979
+ config: { ...this.config }
1980
+ };
1496
1981
  }
1497
- getClientIds() {
1498
- return Array.from(this.clients.keys());
1982
+ clear() {
1983
+ this.entries = [];
1984
+ this.entityIndex.clear();
1985
+ this.oldestVersionIndex.clear();
1986
+ this.newestVersionIndex.clear();
1987
+ this.totalMemory = 0;
1499
1988
  }
1500
- closeAll() {
1501
- for (const clientId of this.clients.keys()) {
1502
- this.closeClient(clientId);
1989
+ dispose() {
1990
+ if (this.cleanupTimer) {
1991
+ clearInterval(this.cleanupTimer);
1992
+ this.cleanupTimer = null;
1993
+ }
1994
+ this.clear();
1995
+ }
1996
+ updateConfig(config) {
1997
+ this.config = { ...this.config, ...config };
1998
+ if (this.cleanupTimer) {
1999
+ clearInterval(this.cleanupTimer);
1503
2000
  }
2001
+ if (this.config.cleanupInterval > 0) {
2002
+ this.cleanupTimer = setInterval(() => this.cleanup(), this.config.cleanupInterval);
2003
+ }
2004
+ this.cleanup();
1504
2005
  }
1505
2006
  }
1506
- function createSSEHandler(config) {
1507
- return new SSEHandler(config);
2007
+ function coalescePatches(patches) {
2008
+ const flatPatches = patches.flat();
2009
+ const pathMap = new Map;
2010
+ for (const op of flatPatches) {
2011
+ const existing = pathMap.get(op.path);
2012
+ if (!existing) {
2013
+ pathMap.set(op.path, op);
2014
+ continue;
2015
+ }
2016
+ switch (op.op) {
2017
+ case "replace":
2018
+ case "add":
2019
+ pathMap.set(op.path, op);
2020
+ break;
2021
+ case "remove":
2022
+ pathMap.set(op.path, op);
2023
+ break;
2024
+ case "move":
2025
+ case "copy":
2026
+ pathMap.set(op.path, op);
2027
+ break;
2028
+ case "test":
2029
+ break;
2030
+ }
2031
+ }
2032
+ const result = Array.from(pathMap.values());
2033
+ result.sort((a, b) => {
2034
+ const depthA = a.path.split("/").length;
2035
+ const depthB = b.path.split("/").length;
2036
+ if (depthA !== depthB)
2037
+ return depthA - depthB;
2038
+ return a.path.localeCompare(b.path);
2039
+ });
2040
+ return result;
2041
+ }
2042
+ function estimatePatchSize(patch) {
2043
+ return JSON.stringify(patch).length;
1508
2044
  }
1509
2045
  export {
2046
+ useContext,
2047
+ tryUseContext,
2048
+ runWithContextAsync,
2049
+ runWithContext,
1510
2050
  router,
1511
2051
  query,
2052
+ optimisticPlugin,
2053
+ opLog,
1512
2054
  mutation,
1513
- createServer,
2055
+ memoryStorage,
2056
+ isOptimisticPlugin,
2057
+ isOpLogPlugin,
2058
+ hasContext,
2059
+ handleWebSSE,
2060
+ handleWebQuery,
2061
+ handleWebMutation,
2062
+ extendContext,
2063
+ estimatePatchSize,
2064
+ createWSHandler,
2065
+ createServerClientProxy,
1514
2066
  createSSEHandler,
1515
- createGraphStateManager,
2067
+ createPluginManager,
2068
+ createHandler,
2069
+ createHTTPHandler,
2070
+ createFrameworkHandler,
2071
+ createContext,
2072
+ createApp,
2073
+ coalescePatches,
1516
2074
  SSEHandler,
1517
- GraphStateManager
2075
+ PluginManager,
2076
+ OperationLog,
2077
+ DEFAULT_STORAGE_CONFIG
1518
2078
  };