@sylphx/lens-server 1.11.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1262 -262
- package/dist/index.js +1714 -1154
- package/package.json +2 -2
- package/src/context/index.test.ts +425 -0
- package/src/context/index.ts +90 -0
- package/src/e2e/server.test.ts +215 -433
- package/src/handlers/framework.ts +294 -0
- package/src/handlers/http.test.ts +215 -0
- package/src/handlers/http.ts +189 -0
- package/src/handlers/index.ts +55 -0
- package/src/handlers/unified.ts +114 -0
- package/src/handlers/ws-types.ts +126 -0
- package/src/handlers/ws.ts +669 -0
- package/src/index.ts +127 -24
- package/src/plugin/index.ts +41 -0
- package/src/plugin/op-log.ts +286 -0
- package/src/plugin/optimistic.ts +375 -0
- package/src/plugin/types.ts +551 -0
- package/src/reconnect/index.ts +9 -0
- package/src/reconnect/operation-log.test.ts +480 -0
- package/src/reconnect/operation-log.ts +450 -0
- package/src/server/create.test.ts +256 -2193
- package/src/server/create.ts +285 -1481
- package/src/server/dataloader.ts +60 -0
- package/src/server/selection.ts +123 -0
- package/src/server/types.ts +306 -0
- package/src/sse/handler.ts +123 -56
- package/src/state/index.ts +9 -11
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +279 -0
- package/src/storage/types.ts +205 -0
- package/src/state/graph-state-manager.test.ts +0 -1105
- package/src/state/graph-state-manager.ts +0 -890
package/dist/index.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/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
88
|
-
if (clientStateMap) {
|
|
89
|
-
clientStateMap.delete(key);
|
|
90
|
-
}
|
|
63
|
+
return true;
|
|
91
64
|
}
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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.
|
|
185
|
+
if (!this.scheduled) {
|
|
186
|
+
this.scheduled = true;
|
|
187
|
+
queueMicrotask(() => this.flush());
|
|
188
|
+
}
|
|
542
189
|
});
|
|
543
190
|
}
|
|
544
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
const result = results[
|
|
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
|
|
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
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
694
|
-
const
|
|
695
|
-
if (
|
|
696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
965
|
+
handleMessage(conn, event.data);
|
|
720
966
|
};
|
|
721
967
|
ws.onclose = () => {
|
|
722
|
-
|
|
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
|
-
|
|
976
|
+
handleHandshake(conn, message);
|
|
731
977
|
break;
|
|
732
978
|
case "subscribe":
|
|
733
|
-
|
|
979
|
+
handleSubscribe(conn, message);
|
|
734
980
|
break;
|
|
735
981
|
case "updateFields":
|
|
736
|
-
|
|
982
|
+
handleUpdateFields(conn, message);
|
|
737
983
|
break;
|
|
738
984
|
case "unsubscribe":
|
|
739
|
-
|
|
985
|
+
handleUnsubscribe(conn, message);
|
|
740
986
|
break;
|
|
741
987
|
case "query":
|
|
742
|
-
|
|
988
|
+
handleQuery(conn, message);
|
|
743
989
|
break;
|
|
744
990
|
case "mutation":
|
|
745
|
-
|
|
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:
|
|
760
|
-
operations:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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 (
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
1090
|
+
newFields = "*";
|
|
1091
|
+
} else if (message.setFields !== undefined) {
|
|
1092
|
+
newFields = message.setFields;
|
|
1093
|
+
} else if (sub.fields === "*") {
|
|
883
1094
|
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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 =
|
|
1109
|
+
sub.fields = newFields;
|
|
908
1110
|
for (const entityKey of sub.entityKeys) {
|
|
909
|
-
const [entity,
|
|
910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
input
|
|
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
|
|
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
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1303
|
+
entityId: String(data.id),
|
|
1202
1304
|
entityData: data
|
|
1203
1305
|
});
|
|
1204
1306
|
}
|
|
1205
1307
|
return results;
|
|
1206
1308
|
}
|
|
1207
|
-
|
|
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) =>
|
|
1322
|
+
return data.map((item) => applySelectionToObject(item, fields));
|
|
1212
1323
|
}
|
|
1213
|
-
return
|
|
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
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
1741
|
+
for (const [key, value] of Object.entries(sugar.merge)) {
|
|
1742
|
+
updateData[key] = value;
|
|
1350
1743
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
-
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
if (
|
|
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
|
-
|
|
1376
|
-
const
|
|
1377
|
-
const
|
|
1378
|
-
for (const
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
|
1881
|
+
return result;
|
|
1386
1882
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
if (
|
|
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
|
|
1889
|
+
return version >= oldest && version <= newest;
|
|
1405
1890
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
loader.clear();
|
|
1409
|
-
}
|
|
1410
|
-
this.loaders.clear();
|
|
1891
|
+
getOldestVersion(entityKey) {
|
|
1892
|
+
return this.oldestVersionIndex.get(entityKey) ?? null;
|
|
1411
1893
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
-
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1486
|
-
const
|
|
1487
|
-
if (
|
|
1488
|
-
|
|
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
|
-
|
|
1495
|
-
return
|
|
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
|
-
|
|
1498
|
-
|
|
1982
|
+
clear() {
|
|
1983
|
+
this.entries = [];
|
|
1984
|
+
this.entityIndex.clear();
|
|
1985
|
+
this.oldestVersionIndex.clear();
|
|
1986
|
+
this.newestVersionIndex.clear();
|
|
1987
|
+
this.totalMemory = 0;
|
|
1499
1988
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
this.
|
|
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
|
|
1507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2067
|
+
createPluginManager,
|
|
2068
|
+
createHandler,
|
|
2069
|
+
createHTTPHandler,
|
|
2070
|
+
createFrameworkHandler,
|
|
2071
|
+
createContext,
|
|
2072
|
+
createApp,
|
|
2073
|
+
coalescePatches,
|
|
1516
2074
|
SSEHandler,
|
|
1517
|
-
|
|
2075
|
+
PluginManager,
|
|
2076
|
+
OperationLog,
|
|
2077
|
+
DEFAULT_STORAGE_CONFIG
|
|
1518
2078
|
};
|