@sylphx/lens-server 2.0.1 → 2.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 +32 -5
- package/dist/index.js +189 -89
- package/package.json +2 -2
- package/src/e2e/server.test.ts +81 -61
- package/src/handlers/framework.ts +4 -3
- package/src/handlers/http.ts +7 -4
- package/src/handlers/ws.ts +18 -10
- package/src/server/create.test.ts +69 -47
- package/src/server/create.ts +198 -91
- package/src/server/selection.ts +82 -3
- package/src/server/types.ts +34 -4
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
import { firstValueFrom } from "@sylphx/lens-core";
|
|
40
41
|
import type { LensServer } from "../server/create.js";
|
|
41
42
|
|
|
42
43
|
// =============================================================================
|
|
@@ -74,7 +75,7 @@ export function createServerClientProxy(server: LensServer): unknown {
|
|
|
74
75
|
},
|
|
75
76
|
async apply(_, __, args) {
|
|
76
77
|
const input = args[0];
|
|
77
|
-
const result = await server.execute({ path, input });
|
|
78
|
+
const result = await firstValueFrom(server.execute({ path, input }));
|
|
78
79
|
|
|
79
80
|
if (result.error) {
|
|
80
81
|
throw result.error;
|
|
@@ -112,7 +113,7 @@ export async function handleWebQuery(
|
|
|
112
113
|
const inputParam = url.searchParams.get("input");
|
|
113
114
|
const input = inputParam ? JSON.parse(inputParam) : undefined;
|
|
114
115
|
|
|
115
|
-
const result = await server.execute({ path, input });
|
|
116
|
+
const result = await firstValueFrom(server.execute({ path, input }));
|
|
116
117
|
|
|
117
118
|
if (result.error) {
|
|
118
119
|
return Response.json({ error: result.error.message }, { status: 400 });
|
|
@@ -147,7 +148,7 @@ export async function handleWebMutation(
|
|
|
147
148
|
const body = (await request.json()) as { input?: unknown };
|
|
148
149
|
const input = body.input;
|
|
149
150
|
|
|
150
|
-
const result = await server.execute({ path, input });
|
|
151
|
+
const result = await firstValueFrom(server.execute({ path, input }));
|
|
151
152
|
|
|
152
153
|
if (result.error) {
|
|
153
154
|
return Response.json({ error: result.error.message }, { status: 400 });
|
package/src/handlers/http.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Works with Bun, Node (with adapter), Vercel, Cloudflare Workers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { firstValueFrom } from "@sylphx/lens-core";
|
|
8
9
|
import type { LensServer } from "../server/create.js";
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
@@ -139,10 +140,12 @@ export function createHTTPHandler(
|
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
const result = await
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const result = await firstValueFrom(
|
|
144
|
+
server.execute({
|
|
145
|
+
path: operationPath,
|
|
146
|
+
input: body.input,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
146
149
|
|
|
147
150
|
if (result.error) {
|
|
148
151
|
return new Response(JSON.stringify({ error: result.error.message }), {
|
package/src/handlers/ws.ts
CHANGED
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import
|
|
25
|
+
import {
|
|
26
|
+
firstValueFrom,
|
|
27
|
+
type ReconnectMessage,
|
|
28
|
+
type ReconnectSubscription,
|
|
29
|
+
} from "@sylphx/lens-core";
|
|
26
30
|
import type { LensServer, WebSocketLike } from "../server/create.js";
|
|
27
31
|
import type {
|
|
28
32
|
ClientConnection,
|
|
@@ -174,7 +178,7 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
174
178
|
// Execute query first to get data
|
|
175
179
|
let result: { data?: unknown; error?: Error };
|
|
176
180
|
try {
|
|
177
|
-
result = await server.execute({ path: operation, input });
|
|
181
|
+
result = await firstValueFrom(server.execute({ path: operation, input }));
|
|
178
182
|
|
|
179
183
|
if (result.error) {
|
|
180
184
|
conn.ws.send(
|
|
@@ -349,10 +353,12 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
349
353
|
// Handle query
|
|
350
354
|
async function handleQuery(conn: ClientConnection, message: QueryMessage): Promise<void> {
|
|
351
355
|
try {
|
|
352
|
-
const result = await
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
const result = await firstValueFrom(
|
|
357
|
+
server.execute({
|
|
358
|
+
path: message.operation,
|
|
359
|
+
input: message.input,
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
356
362
|
|
|
357
363
|
if (result.error) {
|
|
358
364
|
conn.ws.send(
|
|
@@ -389,10 +395,12 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
389
395
|
// Handle mutation
|
|
390
396
|
async function handleMutation(conn: ClientConnection, message: MutationMessage): Promise<void> {
|
|
391
397
|
try {
|
|
392
|
-
const result = await
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
398
|
+
const result = await firstValueFrom(
|
|
399
|
+
server.execute({
|
|
400
|
+
path: message.operation,
|
|
401
|
+
input: message.input,
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
396
404
|
|
|
397
405
|
if (result.error) {
|
|
398
406
|
conn.ws.send(
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, expect, it } from "bun:test";
|
|
9
|
-
import { entity, mutation, query, router } from "@sylphx/lens-core";
|
|
9
|
+
import { entity, firstValueFrom, mutation, query, router } from "@sylphx/lens-core";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { optimisticPlugin } from "../plugin/optimistic.js";
|
|
12
12
|
import { createApp } from "./create.js";
|
|
@@ -190,10 +190,12 @@ describe("execute", () => {
|
|
|
190
190
|
queries: { getUser },
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
const result = await
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
const result = await firstValueFrom(
|
|
194
|
+
server.execute({
|
|
195
|
+
path: "getUser",
|
|
196
|
+
input: { id: "123" },
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
197
199
|
|
|
198
200
|
expect(result.data).toEqual({
|
|
199
201
|
id: "123",
|
|
@@ -208,10 +210,12 @@ describe("execute", () => {
|
|
|
208
210
|
mutations: { createUser },
|
|
209
211
|
});
|
|
210
212
|
|
|
211
|
-
const result = await
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const result = await firstValueFrom(
|
|
214
|
+
server.execute({
|
|
215
|
+
path: "createUser",
|
|
216
|
+
input: { name: "New User", email: "new@example.com" },
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
215
219
|
|
|
216
220
|
expect(result.data).toEqual({
|
|
217
221
|
id: "new-id",
|
|
@@ -226,10 +230,12 @@ describe("execute", () => {
|
|
|
226
230
|
queries: { getUser },
|
|
227
231
|
});
|
|
228
232
|
|
|
229
|
-
const result = await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
const result = await firstValueFrom(
|
|
234
|
+
server.execute({
|
|
235
|
+
path: "unknownOperation",
|
|
236
|
+
input: {},
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
233
239
|
|
|
234
240
|
expect(result.data).toBeUndefined();
|
|
235
241
|
expect(result.error).toBeInstanceOf(Error);
|
|
@@ -241,10 +247,12 @@ describe("execute", () => {
|
|
|
241
247
|
queries: { getUser },
|
|
242
248
|
});
|
|
243
249
|
|
|
244
|
-
const result = await
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
const result = await firstValueFrom(
|
|
251
|
+
server.execute({
|
|
252
|
+
path: "getUser",
|
|
253
|
+
input: { invalid: true }, // Missing required 'id'
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
248
256
|
|
|
249
257
|
expect(result.data).toBeUndefined();
|
|
250
258
|
expect(result.error).toBeInstanceOf(Error);
|
|
@@ -260,10 +268,12 @@ describe("execute", () => {
|
|
|
260
268
|
|
|
261
269
|
const server = createApp({ router: appRouter });
|
|
262
270
|
|
|
263
|
-
const queryResult = await
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
const queryResult = await firstValueFrom(
|
|
272
|
+
server.execute({
|
|
273
|
+
path: "user.get",
|
|
274
|
+
input: { id: "456" },
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
267
277
|
|
|
268
278
|
expect(queryResult.data).toEqual({
|
|
269
279
|
id: "456",
|
|
@@ -271,10 +281,12 @@ describe("execute", () => {
|
|
|
271
281
|
email: "test@example.com",
|
|
272
282
|
});
|
|
273
283
|
|
|
274
|
-
const mutationResult = await
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
const mutationResult = await firstValueFrom(
|
|
285
|
+
server.execute({
|
|
286
|
+
path: "user.create",
|
|
287
|
+
input: { name: "Router User" },
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
278
290
|
|
|
279
291
|
expect(mutationResult.data).toEqual({
|
|
280
292
|
id: "new-id",
|
|
@@ -294,10 +306,12 @@ describe("execute", () => {
|
|
|
294
306
|
queries: { errorQuery },
|
|
295
307
|
});
|
|
296
308
|
|
|
297
|
-
const result = await
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
309
|
+
const result = await firstValueFrom(
|
|
310
|
+
server.execute({
|
|
311
|
+
path: "errorQuery",
|
|
312
|
+
input: { id: "1" },
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
301
315
|
|
|
302
316
|
expect(result.data).toBeUndefined();
|
|
303
317
|
expect(result.error).toBeInstanceOf(Error);
|
|
@@ -309,9 +323,11 @@ describe("execute", () => {
|
|
|
309
323
|
queries: { getUsers },
|
|
310
324
|
});
|
|
311
325
|
|
|
312
|
-
const result = await
|
|
313
|
-
|
|
314
|
-
|
|
326
|
+
const result = await firstValueFrom(
|
|
327
|
+
server.execute({
|
|
328
|
+
path: "getUsers",
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
315
331
|
|
|
316
332
|
expect(result.data).toHaveLength(2);
|
|
317
333
|
});
|
|
@@ -337,10 +353,12 @@ describe("context", () => {
|
|
|
337
353
|
context: () => ({ userId: "user-123", role: "admin" }),
|
|
338
354
|
});
|
|
339
355
|
|
|
340
|
-
await
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
356
|
+
await firstValueFrom(
|
|
357
|
+
server.execute({
|
|
358
|
+
path: "contextQuery",
|
|
359
|
+
input: { id: "1" },
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
344
362
|
|
|
345
363
|
expect(capturedContext).toMatchObject({
|
|
346
364
|
userId: "user-123",
|
|
@@ -366,10 +384,12 @@ describe("context", () => {
|
|
|
366
384
|
},
|
|
367
385
|
});
|
|
368
386
|
|
|
369
|
-
await
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
387
|
+
await firstValueFrom(
|
|
388
|
+
server.execute({
|
|
389
|
+
path: "contextQuery",
|
|
390
|
+
input: { id: "1" },
|
|
391
|
+
}),
|
|
392
|
+
);
|
|
373
393
|
|
|
374
394
|
expect(capturedContext).toMatchObject({
|
|
375
395
|
userId: "async-user",
|
|
@@ -387,13 +407,15 @@ describe("selection", () => {
|
|
|
387
407
|
queries: { getUser },
|
|
388
408
|
});
|
|
389
409
|
|
|
390
|
-
const result = await
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
410
|
+
const result = await firstValueFrom(
|
|
411
|
+
server.execute({
|
|
412
|
+
path: "getUser",
|
|
413
|
+
input: {
|
|
414
|
+
id: "123",
|
|
415
|
+
$select: { name: true },
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
397
419
|
|
|
398
420
|
expect(result.data).toEqual({
|
|
399
421
|
id: "123", // id always included
|
package/src/server/create.ts
CHANGED
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
import {
|
|
19
19
|
type ContextValue,
|
|
20
20
|
createEmit,
|
|
21
|
+
type EmitCommand,
|
|
21
22
|
type EntityDef,
|
|
22
23
|
flattenRouter,
|
|
23
24
|
type InferRouterContext,
|
|
24
25
|
isEntityDef,
|
|
25
26
|
isMutationDef,
|
|
26
27
|
isQueryDef,
|
|
28
|
+
type Observable,
|
|
27
29
|
type ResolverDef,
|
|
28
30
|
type RouterDef,
|
|
29
31
|
toResolverMap,
|
|
@@ -192,109 +194,214 @@ class LensServerImpl<
|
|
|
192
194
|
};
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Execute operation and return Observable.
|
|
199
|
+
*
|
|
200
|
+
* Always returns Observable<LensResult>:
|
|
201
|
+
* - One-shot: emits once, then completes
|
|
202
|
+
* - Streaming: emits multiple times (AsyncIterable or emit-based)
|
|
203
|
+
*/
|
|
204
|
+
execute(op: LensOperation): Observable<LensResult> {
|
|
196
205
|
const { path, input } = op;
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
// Check if operation exists
|
|
208
|
+
const isQuery = !!this.queries[path];
|
|
209
|
+
const isMutation = !!this.mutations[path];
|
|
210
|
+
|
|
211
|
+
if (!isQuery && !isMutation) {
|
|
212
|
+
return {
|
|
213
|
+
subscribe: (observer) => {
|
|
214
|
+
observer.next?.({ error: new Error(`Operation not found: ${path}`) });
|
|
215
|
+
observer.complete?.();
|
|
216
|
+
return { unsubscribe: () => {} };
|
|
217
|
+
},
|
|
218
|
+
};
|
|
210
219
|
}
|
|
211
|
-
}
|
|
212
220
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// =========================================================================
|
|
216
|
-
|
|
217
|
-
private async executeQuery<TInput, TOutput>(name: string, input?: TInput): Promise<TOutput> {
|
|
218
|
-
const queryDef = this.queries[name];
|
|
219
|
-
if (!queryDef) throw new Error(`Query not found: ${name}`);
|
|
220
|
-
|
|
221
|
-
// Extract $select from input
|
|
222
|
-
let select: SelectionObject | undefined;
|
|
223
|
-
let cleanInput = input;
|
|
224
|
-
if (input && typeof input === "object" && "$select" in input) {
|
|
225
|
-
const { $select, ...rest } = input as Record<string, unknown>;
|
|
226
|
-
select = $select as SelectionObject;
|
|
227
|
-
cleanInput = (Object.keys(rest).length > 0 ? rest : undefined) as TInput;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Validate input
|
|
231
|
-
if (queryDef._input && cleanInput !== undefined) {
|
|
232
|
-
const result = queryDef._input.safeParse(cleanInput);
|
|
233
|
-
if (!result.success) {
|
|
234
|
-
throw new Error(`Invalid input: ${JSON.stringify(result.error)}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const context = await this.contextFactory();
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
return await runWithContext(this.ctx, context, async () => {
|
|
242
|
-
const resolver = queryDef._resolve;
|
|
243
|
-
if (!resolver) throw new Error(`Query ${name} has no resolver`);
|
|
244
|
-
|
|
245
|
-
const emit = createEmit(() => {});
|
|
246
|
-
const onCleanup = () => () => {};
|
|
247
|
-
const lensContext = { ...context, emit, onCleanup };
|
|
221
|
+
return this.executeAsObservable(path, input, isQuery);
|
|
222
|
+
}
|
|
248
223
|
|
|
249
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Execute operation and return Observable.
|
|
226
|
+
* Observable allows streaming for AsyncIterable resolvers and emit-based updates.
|
|
227
|
+
*/
|
|
228
|
+
private executeAsObservable(
|
|
229
|
+
path: string,
|
|
230
|
+
input: unknown,
|
|
231
|
+
isQuery: boolean,
|
|
232
|
+
): Observable<LensResult> {
|
|
233
|
+
return {
|
|
234
|
+
subscribe: (observer) => {
|
|
235
|
+
let cancelled = false;
|
|
236
|
+
let currentState: unknown;
|
|
237
|
+
const cleanups: (() => void)[] = [];
|
|
250
238
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
239
|
+
// Run the operation
|
|
240
|
+
(async () => {
|
|
241
|
+
try {
|
|
242
|
+
const def = isQuery ? this.queries[path] : this.mutations[path];
|
|
243
|
+
if (!def) {
|
|
244
|
+
observer.next?.({ error: new Error(`Operation not found: ${path}`) });
|
|
245
|
+
observer.complete?.();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Extract $select from input for queries
|
|
250
|
+
let select: SelectionObject | undefined;
|
|
251
|
+
let cleanInput = input;
|
|
252
|
+
if (isQuery && input && typeof input === "object" && "$select" in input) {
|
|
253
|
+
const { $select, ...rest } = input as Record<string, unknown>;
|
|
254
|
+
select = $select as SelectionObject;
|
|
255
|
+
cleanInput = Object.keys(rest).length > 0 ? rest : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Validate input
|
|
259
|
+
if (def._input && cleanInput !== undefined) {
|
|
260
|
+
const result = def._input.safeParse(cleanInput);
|
|
261
|
+
if (!result.success) {
|
|
262
|
+
observer.next?.({
|
|
263
|
+
error: new Error(`Invalid input: ${JSON.stringify(result.error)}`),
|
|
264
|
+
});
|
|
265
|
+
observer.complete?.();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const context = await this.contextFactory();
|
|
271
|
+
|
|
272
|
+
await runWithContext(this.ctx, context, async () => {
|
|
273
|
+
const resolver = def._resolve;
|
|
274
|
+
if (!resolver) {
|
|
275
|
+
observer.next?.({ error: new Error(`Operation ${path} has no resolver`) });
|
|
276
|
+
observer.complete?.();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Create emit handler that pushes to observer
|
|
281
|
+
const emitHandler = (command: EmitCommand) => {
|
|
282
|
+
if (cancelled) return;
|
|
283
|
+
currentState = this.applyEmitCommand(command, currentState);
|
|
284
|
+
observer.next?.({ data: currentState });
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const emit = createEmit(emitHandler);
|
|
288
|
+
const onCleanup = (fn: () => void) => {
|
|
289
|
+
cleanups.push(fn);
|
|
290
|
+
return () => {
|
|
291
|
+
const idx = cleanups.indexOf(fn);
|
|
292
|
+
if (idx >= 0) cleanups.splice(idx, 1);
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const lensContext = { ...context, emit, onCleanup };
|
|
297
|
+
const result = resolver({ input: cleanInput, ctx: lensContext });
|
|
298
|
+
|
|
299
|
+
if (isAsyncIterable(result)) {
|
|
300
|
+
// Streaming: emit each yielded value
|
|
301
|
+
for await (const value of result) {
|
|
302
|
+
if (cancelled) break;
|
|
303
|
+
currentState = value;
|
|
304
|
+
const processed = await this.processQueryResult(path, value, select);
|
|
305
|
+
observer.next?.({ data: processed });
|
|
306
|
+
}
|
|
307
|
+
if (!cancelled) {
|
|
308
|
+
observer.complete?.();
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// One-shot: emit single value
|
|
312
|
+
const value = await result;
|
|
313
|
+
currentState = value;
|
|
314
|
+
const processed = isQuery
|
|
315
|
+
? await this.processQueryResult(path, value, select)
|
|
316
|
+
: value;
|
|
317
|
+
if (!cancelled) {
|
|
318
|
+
observer.next?.({ data: processed });
|
|
319
|
+
// Don't complete immediately - stay open for potential emit calls
|
|
320
|
+
// For true one-shot, client can unsubscribe after first value
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (!cancelled) {
|
|
326
|
+
observer.next?.({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
327
|
+
observer.complete?.();
|
|
328
|
+
}
|
|
329
|
+
} finally {
|
|
330
|
+
this.clearLoaders();
|
|
259
331
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
332
|
+
})();
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
unsubscribe: () => {
|
|
336
|
+
cancelled = true;
|
|
337
|
+
for (const fn of cleanups) {
|
|
338
|
+
fn();
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
};
|
|
269
344
|
}
|
|
270
345
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Apply emit command to current state.
|
|
348
|
+
*/
|
|
349
|
+
private applyEmitCommand(command: EmitCommand, state: unknown): unknown {
|
|
350
|
+
switch (command.type) {
|
|
351
|
+
case "full":
|
|
352
|
+
if (command.replace) {
|
|
353
|
+
return command.data;
|
|
354
|
+
}
|
|
355
|
+
// Merge mode
|
|
356
|
+
if (state && typeof state === "object" && typeof command.data === "object") {
|
|
357
|
+
return { ...state, ...(command.data as Record<string, unknown>) };
|
|
358
|
+
}
|
|
359
|
+
return command.data;
|
|
360
|
+
|
|
361
|
+
case "field":
|
|
362
|
+
if (state && typeof state === "object") {
|
|
363
|
+
return {
|
|
364
|
+
...(state as Record<string, unknown>),
|
|
365
|
+
[command.field]: command.update.data,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return { [command.field]: command.update.data };
|
|
274
369
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
370
|
+
case "batch":
|
|
371
|
+
if (state && typeof state === "object") {
|
|
372
|
+
const result = { ...(state as Record<string, unknown>) };
|
|
373
|
+
for (const update of command.updates) {
|
|
374
|
+
result[update.field] = update.update.data;
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
return state;
|
|
379
|
+
|
|
380
|
+
case "array": {
|
|
381
|
+
// Array operations - simplified handling
|
|
382
|
+
const arr = Array.isArray(state) ? [...state] : [];
|
|
383
|
+
const op = command.operation;
|
|
384
|
+
switch (op.op) {
|
|
385
|
+
case "push":
|
|
386
|
+
return [...arr, op.item];
|
|
387
|
+
case "unshift":
|
|
388
|
+
return [op.item, ...arr];
|
|
389
|
+
case "insert":
|
|
390
|
+
arr.splice(op.index, 0, op.item);
|
|
391
|
+
return arr;
|
|
392
|
+
case "remove":
|
|
393
|
+
arr.splice(op.index, 1);
|
|
394
|
+
return arr;
|
|
395
|
+
case "update":
|
|
396
|
+
arr[op.index] = op.item;
|
|
397
|
+
return arr;
|
|
398
|
+
default:
|
|
399
|
+
return arr;
|
|
400
|
+
}
|
|
280
401
|
}
|
|
281
|
-
}
|
|
282
402
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
return await runWithContext(this.ctx, context, async () => {
|
|
287
|
-
const resolver = mutationDef._resolve;
|
|
288
|
-
if (!resolver) throw new Error(`Mutation ${name} has no resolver`);
|
|
289
|
-
|
|
290
|
-
const emit = createEmit(() => {});
|
|
291
|
-
const onCleanup = () => () => {};
|
|
292
|
-
const lensContext = { ...context, emit, onCleanup };
|
|
293
|
-
|
|
294
|
-
return (await resolver({ input: input as TInput, ctx: lensContext })) as TOutput;
|
|
295
|
-
});
|
|
296
|
-
} finally {
|
|
297
|
-
this.clearLoaders();
|
|
403
|
+
default:
|
|
404
|
+
return state;
|
|
298
405
|
}
|
|
299
406
|
}
|
|
300
407
|
|