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