@sylphx/lens-server 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +509 -9
- package/dist/index.js +269 -295
- package/package.json +3 -4
- package/src/e2e/server.test.ts +10 -10
- package/src/server/create.test.ts +11 -11
- package/src/server/create.ts +46 -20
- package/src/state/graph-state-manager.test.ts +215 -0
- package/src/state/graph-state-manager.ts +423 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server/create.d.ts +0 -226
- package/dist/server/create.d.ts.map +0 -1
- package/dist/sse/handler.d.ts +0 -78
- package/dist/sse/handler.d.ts.map +0 -1
- package/dist/state/graph-state-manager.d.ts +0 -146
- package/dist/state/graph-state-manager.d.ts.map +0 -1
- package/dist/state/index.d.ts +0 -7
- package/dist/state/index.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,286 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
estimateSize(update) {
|
|
13
|
-
return JSON.stringify(update.data).length;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
var deltaStrategy = {
|
|
17
|
-
name: "delta",
|
|
18
|
-
encode(prev, next) {
|
|
19
|
-
const operations = computeStringDiff(prev, next);
|
|
20
|
-
const diffSize = JSON.stringify(operations).length;
|
|
21
|
-
const valueSize = next.length + 20;
|
|
22
|
-
if (diffSize >= valueSize) {
|
|
23
|
-
return { strategy: "value", data: next };
|
|
24
|
-
}
|
|
25
|
-
return { strategy: "delta", data: operations };
|
|
26
|
-
},
|
|
27
|
-
decode(current, update) {
|
|
28
|
-
if (update.strategy === "value") {
|
|
29
|
-
return update.data;
|
|
30
|
-
}
|
|
31
|
-
const operations = update.data;
|
|
32
|
-
return applyStringDiff(current, operations);
|
|
33
|
-
},
|
|
34
|
-
estimateSize(update) {
|
|
35
|
-
return JSON.stringify(update.data).length;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
function computeStringDiff(prev, next) {
|
|
39
|
-
const operations = [];
|
|
40
|
-
let prefixLen = 0;
|
|
41
|
-
const minLen = Math.min(prev.length, next.length);
|
|
42
|
-
while (prefixLen < minLen && prev[prefixLen] === next[prefixLen]) {
|
|
43
|
-
prefixLen++;
|
|
44
|
-
}
|
|
45
|
-
let suffixLen = 0;
|
|
46
|
-
const remainingPrev = prev.length - prefixLen;
|
|
47
|
-
const remainingNext = next.length - prefixLen;
|
|
48
|
-
const maxSuffix = Math.min(remainingPrev, remainingNext);
|
|
49
|
-
while (suffixLen < maxSuffix && prev[prev.length - 1 - suffixLen] === next[next.length - 1 - suffixLen]) {
|
|
50
|
-
suffixLen++;
|
|
51
|
-
}
|
|
52
|
-
const deleteCount = prev.length - prefixLen - suffixLen;
|
|
53
|
-
const insertText = next.slice(prefixLen, next.length - suffixLen || undefined);
|
|
54
|
-
if (deleteCount > 0 || insertText.length > 0) {
|
|
55
|
-
operations.push({
|
|
56
|
-
position: prefixLen,
|
|
57
|
-
...deleteCount > 0 ? { delete: deleteCount } : {},
|
|
58
|
-
...insertText.length > 0 ? { insert: insertText } : {}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return operations;
|
|
62
|
-
}
|
|
63
|
-
function applyStringDiff(current, operations) {
|
|
64
|
-
let result = current;
|
|
65
|
-
const sortedOps = [...operations].sort((a, b) => b.position - a.position);
|
|
66
|
-
for (const op of sortedOps) {
|
|
67
|
-
const before = result.slice(0, op.position);
|
|
68
|
-
const after = result.slice(op.position + (op.delete ?? 0));
|
|
69
|
-
result = before + (op.insert ?? "") + after;
|
|
70
|
-
}
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
var patchStrategy = {
|
|
74
|
-
name: "patch",
|
|
75
|
-
encode(prev, next) {
|
|
76
|
-
const operations = computeJsonPatch(prev, next);
|
|
77
|
-
const patchSize = JSON.stringify(operations).length;
|
|
78
|
-
const valueSize = JSON.stringify(next).length + 20;
|
|
79
|
-
if (patchSize >= valueSize) {
|
|
80
|
-
return { strategy: "value", data: next };
|
|
81
|
-
}
|
|
82
|
-
return { strategy: "patch", data: operations };
|
|
83
|
-
},
|
|
84
|
-
decode(current, update) {
|
|
85
|
-
if (update.strategy === "value") {
|
|
86
|
-
return update.data;
|
|
87
|
-
}
|
|
88
|
-
const operations = update.data;
|
|
89
|
-
return applyJsonPatch(current, operations);
|
|
90
|
-
},
|
|
91
|
-
estimateSize(update) {
|
|
92
|
-
return JSON.stringify(update.data).length;
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
function computeJsonPatch(prev, next, basePath = "") {
|
|
96
|
-
const operations = [];
|
|
97
|
-
const prevObj = prev;
|
|
98
|
-
const nextObj = next;
|
|
99
|
-
for (const key of Object.keys(prevObj)) {
|
|
100
|
-
if (!(key in nextObj)) {
|
|
101
|
-
operations.push({ op: "remove", path: `${basePath}/${escapeJsonPointer(key)}` });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
for (const [key, nextValue] of Object.entries(nextObj)) {
|
|
105
|
-
const path = `${basePath}/${escapeJsonPointer(key)}`;
|
|
106
|
-
const prevValue = prevObj[key];
|
|
107
|
-
if (!(key in prevObj)) {
|
|
108
|
-
operations.push({ op: "add", path, value: nextValue });
|
|
109
|
-
} else if (!deepEqual(prevValue, nextValue)) {
|
|
110
|
-
if (isPlainObject(prevValue) && isPlainObject(nextValue) && !Array.isArray(prevValue) && !Array.isArray(nextValue)) {
|
|
111
|
-
operations.push(...computeJsonPatch(prevValue, nextValue, path));
|
|
112
|
-
} else {
|
|
113
|
-
operations.push({ op: "replace", path, value: nextValue });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return operations;
|
|
118
|
-
}
|
|
119
|
-
function applyJsonPatch(current, operations) {
|
|
120
|
-
const result = structuredClone(current);
|
|
121
|
-
for (const op of operations) {
|
|
122
|
-
const pathParts = parseJsonPointer(op.path);
|
|
123
|
-
switch (op.op) {
|
|
124
|
-
case "add":
|
|
125
|
-
case "replace":
|
|
126
|
-
setValueAtPath(result, pathParts, op.value);
|
|
127
|
-
break;
|
|
128
|
-
case "remove":
|
|
129
|
-
removeValueAtPath(result, pathParts);
|
|
130
|
-
break;
|
|
131
|
-
case "move":
|
|
132
|
-
if (op.from) {
|
|
133
|
-
const fromParts = parseJsonPointer(op.from);
|
|
134
|
-
const value = getValueAtPath(result, fromParts);
|
|
135
|
-
removeValueAtPath(result, fromParts);
|
|
136
|
-
setValueAtPath(result, pathParts, value);
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
case "copy":
|
|
140
|
-
if (op.from) {
|
|
141
|
-
const fromParts = parseJsonPointer(op.from);
|
|
142
|
-
const value = structuredClone(getValueAtPath(result, fromParts));
|
|
143
|
-
setValueAtPath(result, pathParts, value);
|
|
144
|
-
}
|
|
145
|
-
break;
|
|
146
|
-
case "test":
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return result;
|
|
151
|
-
}
|
|
152
|
-
var THRESHOLDS = {
|
|
153
|
-
STRING_DELTA_MIN: 100,
|
|
154
|
-
OBJECT_PATCH_MIN: 50
|
|
155
|
-
};
|
|
156
|
-
function selectStrategy(prev, next) {
|
|
157
|
-
if (typeof prev === "string" && typeof next === "string") {
|
|
158
|
-
if (next.length >= THRESHOLDS.STRING_DELTA_MIN) {
|
|
159
|
-
return deltaStrategy;
|
|
160
|
-
}
|
|
161
|
-
return valueStrategy;
|
|
162
|
-
}
|
|
163
|
-
if (typeof next !== "object" || next === null) {
|
|
164
|
-
return valueStrategy;
|
|
165
|
-
}
|
|
166
|
-
if (isPlainObject(prev) && isPlainObject(next)) {
|
|
167
|
-
const prevSize = JSON.stringify(prev).length;
|
|
168
|
-
if (prevSize >= THRESHOLDS.OBJECT_PATCH_MIN) {
|
|
169
|
-
return patchStrategy;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return valueStrategy;
|
|
173
|
-
}
|
|
174
|
-
function createUpdate(prev, next) {
|
|
175
|
-
const strategy = selectStrategy(prev, next);
|
|
176
|
-
return strategy.encode(prev, next);
|
|
177
|
-
}
|
|
178
|
-
function isPlainObject(value) {
|
|
179
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
180
|
-
}
|
|
181
|
-
function deepEqual(a, b) {
|
|
182
|
-
if (a === b)
|
|
183
|
-
return true;
|
|
184
|
-
if (typeof a !== typeof b)
|
|
185
|
-
return false;
|
|
186
|
-
if (typeof a !== "object" || a === null || b === null)
|
|
187
|
-
return false;
|
|
188
|
-
const aKeys = Object.keys(a);
|
|
189
|
-
const bKeys = Object.keys(b);
|
|
190
|
-
if (aKeys.length !== bKeys.length)
|
|
191
|
-
return false;
|
|
192
|
-
for (const key of aKeys) {
|
|
193
|
-
if (!deepEqual(a[key], b[key])) {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
function escapeJsonPointer(str) {
|
|
200
|
-
return str.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
201
|
-
}
|
|
202
|
-
function parseJsonPointer(path) {
|
|
203
|
-
if (!path || path === "/")
|
|
204
|
-
return [];
|
|
205
|
-
return path.slice(1).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
206
|
-
}
|
|
207
|
-
function getValueAtPath(obj, path) {
|
|
208
|
-
let current = obj;
|
|
209
|
-
for (const key of path) {
|
|
210
|
-
if (current === null || typeof current !== "object")
|
|
211
|
-
return;
|
|
212
|
-
current = current[key];
|
|
213
|
-
}
|
|
214
|
-
return current;
|
|
215
|
-
}
|
|
216
|
-
function setValueAtPath(obj, path, value) {
|
|
217
|
-
if (path.length === 0)
|
|
218
|
-
return;
|
|
219
|
-
let current = obj;
|
|
220
|
-
for (let i = 0;i < path.length - 1; i++) {
|
|
221
|
-
const key = path[i];
|
|
222
|
-
if (!(key in current) || typeof current[key] !== "object") {
|
|
223
|
-
current[key] = {};
|
|
224
|
-
}
|
|
225
|
-
current = current[key];
|
|
226
|
-
}
|
|
227
|
-
current[path[path.length - 1]] = value;
|
|
228
|
-
}
|
|
229
|
-
function removeValueAtPath(obj, path) {
|
|
230
|
-
if (path.length === 0)
|
|
231
|
-
return;
|
|
232
|
-
let current = obj;
|
|
233
|
-
for (let i = 0;i < path.length - 1; i++) {
|
|
234
|
-
const key = path[i];
|
|
235
|
-
if (!(key in current) || typeof current[key] !== "object")
|
|
236
|
-
return;
|
|
237
|
-
current = current[key];
|
|
238
|
-
}
|
|
239
|
-
delete current[path[path.length - 1]];
|
|
240
|
-
}
|
|
241
|
-
function isQueryDef(value) {
|
|
242
|
-
return typeof value === "object" && value !== null && value._type === "query";
|
|
243
|
-
}
|
|
244
|
-
function isMutationDef(value) {
|
|
245
|
-
return typeof value === "object" && value !== null && value._type === "mutation";
|
|
246
|
-
}
|
|
247
|
-
function isRouterDef(value) {
|
|
248
|
-
return typeof value === "object" && value !== null && value._type === "router";
|
|
249
|
-
}
|
|
250
|
-
function flattenRouter(routerDef, prefix = "") {
|
|
251
|
-
const result = new Map;
|
|
252
|
-
for (const [key, value] of Object.entries(routerDef._routes)) {
|
|
253
|
-
const path = prefix ? `${prefix}.${key}` : key;
|
|
254
|
-
if (isRouterDef(value)) {
|
|
255
|
-
const nested = flattenRouter(value, path);
|
|
256
|
-
for (const [nestedPath, procedure] of nested) {
|
|
257
|
-
result.set(nestedPath, procedure);
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
result.set(path, value);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
function isBatchResolver(resolver) {
|
|
266
|
-
return typeof resolver === "object" && resolver !== null && "batch" in resolver;
|
|
267
|
-
}
|
|
268
|
-
var globalContextStore = new AsyncLocalStorage;
|
|
269
|
-
function createContext() {
|
|
270
|
-
return globalContextStore;
|
|
271
|
-
}
|
|
272
|
-
function runWithContext(_context, value, fn) {
|
|
273
|
-
return globalContextStore.run(value, fn);
|
|
274
|
-
}
|
|
275
|
-
function makeEntityKey(entity2, id) {
|
|
276
|
-
return `${entity2}:${id}`;
|
|
277
|
-
}
|
|
1
|
+
// src/server/create.ts
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
createEmit,
|
|
5
|
+
createUpdate as createUpdate2,
|
|
6
|
+
flattenRouter,
|
|
7
|
+
isBatchResolver,
|
|
8
|
+
isMutationDef,
|
|
9
|
+
isQueryDef,
|
|
10
|
+
runWithContext
|
|
11
|
+
} from "@sylphx/lens-core";
|
|
278
12
|
|
|
279
13
|
// src/state/graph-state-manager.ts
|
|
14
|
+
import {
|
|
15
|
+
createUpdate,
|
|
16
|
+
applyUpdate,
|
|
17
|
+
makeEntityKey
|
|
18
|
+
} from "@sylphx/lens-core";
|
|
19
|
+
|
|
280
20
|
class GraphStateManager {
|
|
281
21
|
clients = new Map;
|
|
282
22
|
canonical = new Map;
|
|
23
|
+
canonicalArrays = new Map;
|
|
283
24
|
clientStates = new Map;
|
|
25
|
+
clientArrayStates = new Map;
|
|
284
26
|
entitySubscribers = new Map;
|
|
285
27
|
config;
|
|
286
28
|
constructor(config = {}) {
|
|
@@ -289,6 +31,7 @@ class GraphStateManager {
|
|
|
289
31
|
addClient(client) {
|
|
290
32
|
this.clients.set(client.id, client);
|
|
291
33
|
this.clientStates.set(client.id, new Map);
|
|
34
|
+
this.clientArrayStates.set(client.id, new Map);
|
|
292
35
|
}
|
|
293
36
|
removeClient(clientId) {
|
|
294
37
|
for (const [key, subscribers] of this.entitySubscribers) {
|
|
@@ -299,6 +42,7 @@ class GraphStateManager {
|
|
|
299
42
|
}
|
|
300
43
|
this.clients.delete(clientId);
|
|
301
44
|
this.clientStates.delete(clientId);
|
|
45
|
+
this.clientArrayStates.delete(clientId);
|
|
302
46
|
}
|
|
303
47
|
subscribe(clientId, entity, id, fields = "*") {
|
|
304
48
|
const key = this.makeKey(entity, id);
|
|
@@ -361,6 +105,148 @@ class GraphStateManager {
|
|
|
361
105
|
this.pushToClient(clientId, entity, id, key, currentCanonical);
|
|
362
106
|
}
|
|
363
107
|
}
|
|
108
|
+
emitField(entity, id, field, update) {
|
|
109
|
+
const key = this.makeKey(entity, id);
|
|
110
|
+
let currentCanonical = this.canonical.get(key);
|
|
111
|
+
if (!currentCanonical) {
|
|
112
|
+
currentCanonical = {};
|
|
113
|
+
}
|
|
114
|
+
const oldValue = currentCanonical[field];
|
|
115
|
+
const newValue = applyUpdate(oldValue, update);
|
|
116
|
+
currentCanonical = { ...currentCanonical, [field]: newValue };
|
|
117
|
+
this.canonical.set(key, currentCanonical);
|
|
118
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
119
|
+
if (!subscribers)
|
|
120
|
+
return;
|
|
121
|
+
for (const clientId of subscribers) {
|
|
122
|
+
this.pushFieldToClient(clientId, entity, id, key, field, newValue);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
emitBatch(entity, id, updates) {
|
|
126
|
+
const key = this.makeKey(entity, id);
|
|
127
|
+
let currentCanonical = this.canonical.get(key);
|
|
128
|
+
if (!currentCanonical) {
|
|
129
|
+
currentCanonical = {};
|
|
130
|
+
}
|
|
131
|
+
const changedFields = [];
|
|
132
|
+
for (const { field, update } of updates) {
|
|
133
|
+
const oldValue = currentCanonical[field];
|
|
134
|
+
const newValue = applyUpdate(oldValue, update);
|
|
135
|
+
currentCanonical[field] = newValue;
|
|
136
|
+
changedFields.push(field);
|
|
137
|
+
}
|
|
138
|
+
this.canonical.set(key, currentCanonical);
|
|
139
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
140
|
+
if (!subscribers)
|
|
141
|
+
return;
|
|
142
|
+
for (const clientId of subscribers) {
|
|
143
|
+
this.pushFieldsToClient(clientId, entity, id, key, changedFields, currentCanonical);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
processCommand(entity, id, command) {
|
|
147
|
+
switch (command.type) {
|
|
148
|
+
case "full":
|
|
149
|
+
this.emit(entity, id, command.data, {
|
|
150
|
+
replace: command.replace
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
case "field":
|
|
154
|
+
this.emitField(entity, id, command.field, command.update);
|
|
155
|
+
break;
|
|
156
|
+
case "batch":
|
|
157
|
+
this.emitBatch(entity, id, command.updates);
|
|
158
|
+
break;
|
|
159
|
+
case "array":
|
|
160
|
+
this.emitArrayOperation(entity, id, command.operation);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
emitArray(entity, id, items) {
|
|
165
|
+
const key = this.makeKey(entity, id);
|
|
166
|
+
this.canonicalArrays.set(key, [...items]);
|
|
167
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
168
|
+
if (!subscribers)
|
|
169
|
+
return;
|
|
170
|
+
for (const clientId of subscribers) {
|
|
171
|
+
this.pushArrayToClient(clientId, entity, id, key, items);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
emitArrayOperation(entity, id, operation) {
|
|
175
|
+
const key = this.makeKey(entity, id);
|
|
176
|
+
let currentArray = this.canonicalArrays.get(key);
|
|
177
|
+
if (!currentArray) {
|
|
178
|
+
currentArray = [];
|
|
179
|
+
}
|
|
180
|
+
const newArray = this.applyArrayOperation([...currentArray], operation);
|
|
181
|
+
this.canonicalArrays.set(key, newArray);
|
|
182
|
+
const subscribers = this.entitySubscribers.get(key);
|
|
183
|
+
if (!subscribers)
|
|
184
|
+
return;
|
|
185
|
+
for (const clientId of subscribers) {
|
|
186
|
+
this.pushArrayToClient(clientId, entity, id, key, newArray);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
applyArrayOperation(array, operation) {
|
|
190
|
+
switch (operation.op) {
|
|
191
|
+
case "push":
|
|
192
|
+
return [...array, operation.item];
|
|
193
|
+
case "unshift":
|
|
194
|
+
return [operation.item, ...array];
|
|
195
|
+
case "insert":
|
|
196
|
+
return [
|
|
197
|
+
...array.slice(0, operation.index),
|
|
198
|
+
operation.item,
|
|
199
|
+
...array.slice(operation.index)
|
|
200
|
+
];
|
|
201
|
+
case "remove":
|
|
202
|
+
return [...array.slice(0, operation.index), ...array.slice(operation.index + 1)];
|
|
203
|
+
case "removeById": {
|
|
204
|
+
const idx = array.findIndex((item) => typeof item === "object" && item !== null && ("id" in item) && item.id === operation.id);
|
|
205
|
+
if (idx === -1)
|
|
206
|
+
return array;
|
|
207
|
+
return [...array.slice(0, idx), ...array.slice(idx + 1)];
|
|
208
|
+
}
|
|
209
|
+
case "update":
|
|
210
|
+
return array.map((item, i) => i === operation.index ? operation.item : item);
|
|
211
|
+
case "updateById":
|
|
212
|
+
return array.map((item) => typeof item === "object" && item !== null && ("id" in item) && item.id === operation.id ? operation.item : item);
|
|
213
|
+
case "merge":
|
|
214
|
+
return array.map((item, i) => i === operation.index && typeof item === "object" && item !== null ? { ...item, ...operation.partial } : item);
|
|
215
|
+
case "mergeById":
|
|
216
|
+
return array.map((item) => typeof item === "object" && item !== null && ("id" in item) && item.id === operation.id ? { ...item, ...operation.partial } : item);
|
|
217
|
+
default:
|
|
218
|
+
return array;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
pushArrayToClient(clientId, entity, id, key, newArray) {
|
|
222
|
+
const client = this.clients.get(clientId);
|
|
223
|
+
if (!client)
|
|
224
|
+
return;
|
|
225
|
+
const clientArrayStateMap = this.clientArrayStates.get(clientId);
|
|
226
|
+
if (!clientArrayStateMap)
|
|
227
|
+
return;
|
|
228
|
+
let clientArrayState = clientArrayStateMap.get(key);
|
|
229
|
+
if (!clientArrayState) {
|
|
230
|
+
clientArrayState = { lastState: [] };
|
|
231
|
+
clientArrayStateMap.set(key, clientArrayState);
|
|
232
|
+
}
|
|
233
|
+
const { lastState } = clientArrayState;
|
|
234
|
+
if (JSON.stringify(lastState) === JSON.stringify(newArray)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
client.send({
|
|
238
|
+
type: "update",
|
|
239
|
+
entity,
|
|
240
|
+
id,
|
|
241
|
+
updates: {
|
|
242
|
+
_items: { strategy: "value", data: newArray }
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
clientArrayState.lastState = [...newArray];
|
|
246
|
+
}
|
|
247
|
+
getArrayState(entity, id) {
|
|
248
|
+
return this.canonicalArrays.get(this.makeKey(entity, id));
|
|
249
|
+
}
|
|
364
250
|
getState(entity, id) {
|
|
365
251
|
return this.canonical.get(this.makeKey(entity, id));
|
|
366
252
|
}
|
|
@@ -408,6 +294,77 @@ class GraphStateManager {
|
|
|
408
294
|
}
|
|
409
295
|
}
|
|
410
296
|
}
|
|
297
|
+
pushFieldToClient(clientId, entity, id, key, field, newValue) {
|
|
298
|
+
const client = this.clients.get(clientId);
|
|
299
|
+
if (!client)
|
|
300
|
+
return;
|
|
301
|
+
const clientStateMap = this.clientStates.get(clientId);
|
|
302
|
+
if (!clientStateMap)
|
|
303
|
+
return;
|
|
304
|
+
const clientEntityState = clientStateMap.get(key);
|
|
305
|
+
if (!clientEntityState)
|
|
306
|
+
return;
|
|
307
|
+
const { lastState, fields } = clientEntityState;
|
|
308
|
+
if (fields !== "*" && !fields.has(field)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const oldValue = lastState[field];
|
|
312
|
+
if (oldValue === newValue)
|
|
313
|
+
return;
|
|
314
|
+
if (typeof oldValue === "object" && typeof newValue === "object" && JSON.stringify(oldValue) === JSON.stringify(newValue)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const update = createUpdate(oldValue, newValue);
|
|
318
|
+
client.send({
|
|
319
|
+
type: "update",
|
|
320
|
+
entity,
|
|
321
|
+
id,
|
|
322
|
+
updates: { [field]: update }
|
|
323
|
+
});
|
|
324
|
+
clientEntityState.lastState[field] = newValue;
|
|
325
|
+
}
|
|
326
|
+
pushFieldsToClient(clientId, entity, id, key, changedFields, newState) {
|
|
327
|
+
const client = this.clients.get(clientId);
|
|
328
|
+
if (!client)
|
|
329
|
+
return;
|
|
330
|
+
const clientStateMap = this.clientStates.get(clientId);
|
|
331
|
+
if (!clientStateMap)
|
|
332
|
+
return;
|
|
333
|
+
const clientEntityState = clientStateMap.get(key);
|
|
334
|
+
if (!clientEntityState)
|
|
335
|
+
return;
|
|
336
|
+
const { lastState, fields } = clientEntityState;
|
|
337
|
+
const updates = {};
|
|
338
|
+
let hasChanges = false;
|
|
339
|
+
for (const field of changedFields) {
|
|
340
|
+
if (fields !== "*" && !fields.has(field)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const oldValue = lastState[field];
|
|
344
|
+
const newValue = newState[field];
|
|
345
|
+
if (oldValue === newValue)
|
|
346
|
+
continue;
|
|
347
|
+
if (typeof oldValue === "object" && typeof newValue === "object" && JSON.stringify(oldValue) === JSON.stringify(newValue)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const update = createUpdate(oldValue, newValue);
|
|
351
|
+
updates[field] = update;
|
|
352
|
+
hasChanges = true;
|
|
353
|
+
}
|
|
354
|
+
if (!hasChanges)
|
|
355
|
+
return;
|
|
356
|
+
client.send({
|
|
357
|
+
type: "update",
|
|
358
|
+
entity,
|
|
359
|
+
id,
|
|
360
|
+
updates
|
|
361
|
+
});
|
|
362
|
+
for (const field of changedFields) {
|
|
363
|
+
if (newState[field] !== undefined) {
|
|
364
|
+
clientEntityState.lastState[field] = newState[field];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
411
368
|
sendInitialData(clientId, entity, id, state, fields) {
|
|
412
369
|
const client = this.clients.get(clientId);
|
|
413
370
|
if (!client)
|
|
@@ -460,7 +417,9 @@ class GraphStateManager {
|
|
|
460
417
|
clear() {
|
|
461
418
|
this.clients.clear();
|
|
462
419
|
this.canonical.clear();
|
|
420
|
+
this.canonicalArrays.clear();
|
|
463
421
|
this.clientStates.clear();
|
|
422
|
+
this.clientArrayStates.clear();
|
|
464
423
|
this.entitySubscribers.clear();
|
|
465
424
|
}
|
|
466
425
|
}
|
|
@@ -770,21 +729,31 @@ class LensServerImpl {
|
|
|
770
729
|
if (!resolver) {
|
|
771
730
|
throw new Error(`Query ${sub.operation} has no resolver`);
|
|
772
731
|
}
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (idx >= 0)
|
|
781
|
-
sub.cleanups.splice(idx, 1);
|
|
782
|
-
};
|
|
732
|
+
const emit = createEmit((command) => {
|
|
733
|
+
const entityName = this.getEntityNameFromOutput(queryDef._output);
|
|
734
|
+
if (entityName) {
|
|
735
|
+
const entities = this.extractEntities(entityName, command.type === "full" ? command.data : {});
|
|
736
|
+
for (const { entity, id } of entities) {
|
|
737
|
+
this.stateManager.processCommand(entity, id, command);
|
|
738
|
+
}
|
|
783
739
|
}
|
|
740
|
+
if (command.type === "full") {
|
|
741
|
+
emitData(command.data);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
const onCleanup = (fn) => {
|
|
745
|
+
sub.cleanups.push(fn);
|
|
746
|
+
return () => {
|
|
747
|
+
const idx = sub.cleanups.indexOf(fn);
|
|
748
|
+
if (idx >= 0)
|
|
749
|
+
sub.cleanups.splice(idx, 1);
|
|
750
|
+
};
|
|
784
751
|
};
|
|
785
752
|
const result = resolver({
|
|
786
753
|
input: sub.input,
|
|
787
|
-
ctx:
|
|
754
|
+
ctx: context,
|
|
755
|
+
emit,
|
|
756
|
+
onCleanup
|
|
788
757
|
});
|
|
789
758
|
if (isAsyncIterable(result)) {
|
|
790
759
|
for await (const value of result) {
|
|
@@ -933,13 +902,14 @@ class LensServerImpl {
|
|
|
933
902
|
if (!resolver) {
|
|
934
903
|
throw new Error(`Query ${name} has no resolver`);
|
|
935
904
|
}
|
|
936
|
-
const
|
|
905
|
+
const emit = createEmit(() => {});
|
|
906
|
+
const onCleanup = () => () => {};
|
|
907
|
+
const result = resolver({
|
|
937
908
|
input: cleanInput,
|
|
938
909
|
ctx: context,
|
|
939
|
-
emit
|
|
940
|
-
onCleanup
|
|
941
|
-
};
|
|
942
|
-
const result = resolver(resolverCtx);
|
|
910
|
+
emit,
|
|
911
|
+
onCleanup
|
|
912
|
+
});
|
|
943
913
|
let data;
|
|
944
914
|
if (isAsyncIterable(result)) {
|
|
945
915
|
for await (const value of result) {
|
|
@@ -976,9 +946,13 @@ class LensServerImpl {
|
|
|
976
946
|
if (!resolver) {
|
|
977
947
|
throw new Error(`Mutation ${name} has no resolver`);
|
|
978
948
|
}
|
|
949
|
+
const emit = createEmit(() => {});
|
|
950
|
+
const onCleanup = () => () => {};
|
|
979
951
|
const result = await resolver({
|
|
980
952
|
input,
|
|
981
|
-
ctx: context
|
|
953
|
+
ctx: context,
|
|
954
|
+
emit,
|
|
955
|
+
onCleanup
|
|
982
956
|
});
|
|
983
957
|
const entityName = this.getEntityNameFromMutation(name);
|
|
984
958
|
const entities = this.extractEntities(entityName, result);
|
|
@@ -1301,7 +1275,7 @@ class LensServerImpl {
|
|
|
1301
1275
|
const oldValue = oldObj[key];
|
|
1302
1276
|
const newValue = newObj[key];
|
|
1303
1277
|
if (!this.deepEqual(oldValue, newValue)) {
|
|
1304
|
-
updates[key] =
|
|
1278
|
+
updates[key] = createUpdate2(oldValue, newValue);
|
|
1305
1279
|
}
|
|
1306
1280
|
}
|
|
1307
1281
|
return Object.keys(updates).length > 0 ? updates : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Server runtime for Lens API framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "
|
|
16
|
-
"build:types": "tsc --emitDeclarationOnly --outDir ./dist",
|
|
15
|
+
"build": "bunup",
|
|
17
16
|
"typecheck": "tsc --noEmit",
|
|
18
17
|
"test": "bun test"
|
|
19
18
|
},
|
|
@@ -30,7 +29,7 @@
|
|
|
30
29
|
"author": "SylphxAI",
|
|
31
30
|
"license": "MIT",
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"@sylphx/lens-core": "^1.0
|
|
32
|
+
"@sylphx/lens-core": "^1.2.0"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"typescript": "^5.9.3",
|