@sylphx/lens-server 2.13.1 → 2.14.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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { describe, expect, it } from "bun:test";
11
- import { entity, firstValueFrom, lens, mutation, query, t } from "@sylphx/lens-core";
11
+ import { entity, firstValueFrom, isError, isSnapshot, lens, mutation, query, t } from "@sylphx/lens-core";
12
12
  import { z } from "zod";
13
13
  import { optimisticPlugin } from "../plugin/optimistic.js";
14
14
  import { createApp } from "../server/create.js";
@@ -60,8 +60,10 @@ describe("E2E - Basic Operations", () => {
60
60
 
61
61
  const result = await firstValueFrom(server.execute({ path: "getUsers" }));
62
62
 
63
- expect(result.error).toBeUndefined();
64
- expect(result.data).toEqual(mockUsers);
63
+ expect(isSnapshot(result)).toBe(true);
64
+ if (isSnapshot(result)) {
65
+ expect(result.data).toEqual(mockUsers);
66
+ }
65
67
  });
66
68
 
67
69
  it("query with input", async () => {
@@ -86,8 +88,10 @@ describe("E2E - Basic Operations", () => {
86
88
  }),
87
89
  );
88
90
 
89
- expect(result.error).toBeUndefined();
90
- expect(result.data).toEqual(mockUsers[0]);
91
+ expect(isSnapshot(result)).toBe(true);
92
+ if (isSnapshot(result)) {
93
+ expect(result.data).toEqual(mockUsers[0]);
94
+ }
91
95
  });
92
96
 
93
97
  it("mutation", async () => {
@@ -113,13 +117,15 @@ describe("E2E - Basic Operations", () => {
113
117
  }),
114
118
  );
115
119
 
116
- expect(result.error).toBeUndefined();
117
- expect(result.data).toEqual({
118
- id: "user-new",
119
- name: "Charlie",
120
- email: "charlie@example.com",
121
- status: "offline",
122
- });
120
+ expect(isSnapshot(result)).toBe(true);
121
+ if (isSnapshot(result)) {
122
+ expect(result.data).toEqual({
123
+ id: "user-new",
124
+ name: "Charlie",
125
+ email: "charlie@example.com",
126
+ status: "offline",
127
+ });
128
+ }
123
129
  });
124
130
 
125
131
  it("handles query errors", async () => {
@@ -140,9 +146,10 @@ describe("E2E - Basic Operations", () => {
140
146
  }),
141
147
  );
142
148
 
143
- expect(result.data).toBeUndefined();
144
- expect(result.error).toBeInstanceOf(Error);
145
- expect(result.error?.message).toBe("Query failed");
149
+ expect(isError(result)).toBe(true);
150
+ if (isError(result)) {
151
+ expect(result.error).toBe("Query failed");
152
+ }
146
153
  });
147
154
 
148
155
  it("handles unknown operation", async () => {
@@ -155,8 +162,10 @@ describe("E2E - Basic Operations", () => {
155
162
  }),
156
163
  );
157
164
 
158
- expect(result.data).toBeUndefined();
159
- expect(result.error?.message).toContain("not found");
165
+ expect(isError(result)).toBe(true);
166
+ if (isError(result)) {
167
+ expect(result.error).toContain("not found");
168
+ }
160
169
  });
161
170
  });
162
171
 
@@ -254,15 +263,17 @@ describe("E2E - Selection", () => {
254
263
  }),
255
264
  );
256
265
 
257
- expect(result.error).toBeUndefined();
258
- // Should include id (always) and selected fields
259
- expect(result.data).toEqual({
260
- id: "user-1",
261
- name: "Alice",
262
- });
263
- // Should not include unselected fields
264
- expect((result.data as Record<string, unknown>).email).toBeUndefined();
265
- expect((result.data as Record<string, unknown>).status).toBeUndefined();
266
+ expect(isSnapshot(result)).toBe(true);
267
+ if (isSnapshot(result)) {
268
+ // Should include id (always) and selected fields
269
+ expect(result.data).toEqual({
270
+ id: "user-1",
271
+ name: "Alice",
272
+ });
273
+ // Should not include unselected fields
274
+ expect((result.data as Record<string, unknown>).email).toBeUndefined();
275
+ expect((result.data as Record<string, unknown>).status).toBeUndefined();
276
+ }
266
277
  });
267
278
 
268
279
  it("includes id by default in selection", async () => {
@@ -286,10 +297,13 @@ describe("E2E - Selection", () => {
286
297
  }),
287
298
  );
288
299
 
289
- expect(result.data).toEqual({
290
- id: "user-1",
291
- email: "alice@example.com",
292
- });
300
+ expect(isSnapshot(result)).toBe(true);
301
+ if (isSnapshot(result)) {
302
+ expect(result.data).toEqual({
303
+ id: "user-1",
304
+ email: "alice@example.com",
305
+ });
306
+ }
293
307
  });
294
308
  });
295
309
 
@@ -351,15 +365,17 @@ describe("E2E - Entity Resolvers", () => {
351
365
  }),
352
366
  );
353
367
 
354
- expect(result.error).toBeUndefined();
355
- expect(result.data).toMatchObject({
356
- id: "user-1",
357
- name: "Alice",
358
- posts: [
359
- { id: "post-1", title: "Hello World" },
360
- { id: "post-2", title: "Second Post" },
361
- ],
362
- });
368
+ expect(isSnapshot(result)).toBe(true);
369
+ if (isSnapshot(result)) {
370
+ expect(result.data).toMatchObject({
371
+ id: "user-1",
372
+ name: "Alice",
373
+ posts: [
374
+ { id: "post-1", title: "Hello World" },
375
+ { id: "post-2", title: "Second Post" },
376
+ ],
377
+ });
378
+ }
363
379
  });
364
380
 
365
381
  it("handles DataLoader batching for entity resolvers", async () => {
@@ -414,10 +430,12 @@ describe("E2E - Entity Resolvers", () => {
414
430
  }),
415
431
  );
416
432
 
417
- expect(result.error).toBeUndefined();
418
- // Resolvers are called - exact count depends on DataLoader batching behavior
419
- expect(batchCallCount).toBeGreaterThanOrEqual(2);
420
- expect(result.data).toHaveLength(2);
433
+ expect(isSnapshot(result)).toBe(true);
434
+ if (isSnapshot(result)) {
435
+ // Resolvers are called - exact count depends on DataLoader batching behavior
436
+ expect(batchCallCount).toBeGreaterThanOrEqual(2);
437
+ expect(result.data).toHaveLength(2);
438
+ }
421
439
  });
422
440
  });
423
441
 
@@ -37,7 +37,7 @@
37
37
  * ```
38
38
  */
39
39
 
40
- import { firstValueFrom } from "@sylphx/lens-core";
40
+ import { firstValueFrom, isError, isSnapshot } from "@sylphx/lens-core";
41
41
  import type { LensServer } from "../server/create.js";
42
42
 
43
43
  // =============================================================================
@@ -77,11 +77,16 @@ export function createServerClientProxy(server: LensServer): unknown {
77
77
  const input = args[0];
78
78
  const result = await firstValueFrom(server.execute({ path, input }));
79
79
 
80
- if (result.error) {
81
- throw result.error;
80
+ if (isError(result)) {
81
+ throw new Error(result.error);
82
82
  }
83
83
 
84
- return result.data;
84
+ if (isSnapshot(result)) {
85
+ return result.data;
86
+ }
87
+
88
+ // ops message - shouldn't happen for one-shot calls
89
+ return null;
85
90
  },
86
91
  });
87
92
  }
@@ -115,11 +120,16 @@ export async function handleWebQuery(
115
120
 
116
121
  const result = await firstValueFrom(server.execute({ path, input }));
117
122
 
118
- if (result.error) {
119
- return Response.json({ error: result.error.message }, { status: 400 });
123
+ if (isError(result)) {
124
+ return Response.json({ error: result.error }, { status: 400 });
125
+ }
126
+
127
+ if (isSnapshot(result)) {
128
+ return Response.json({ data: result.data });
120
129
  }
121
130
 
122
- return Response.json({ data: result.data });
131
+ // ops message - forward as-is
132
+ return Response.json(result);
123
133
  } catch (error) {
124
134
  return Response.json(
125
135
  { error: error instanceof Error ? error.message : "Unknown error" },
@@ -150,11 +160,16 @@ export async function handleWebMutation(
150
160
 
151
161
  const result = await firstValueFrom(server.execute({ path, input }));
152
162
 
153
- if (result.error) {
154
- return Response.json({ error: result.error.message }, { status: 400 });
163
+ if (isError(result)) {
164
+ return Response.json({ error: result.error }, { status: 400 });
165
+ }
166
+
167
+ if (isSnapshot(result)) {
168
+ return Response.json({ data: result.data });
155
169
  }
156
170
 
157
- return Response.json({ data: result.data });
171
+ // ops message - forward as-is
172
+ return Response.json(result);
158
173
  } catch (error) {
159
174
  return Response.json(
160
175
  { error: error instanceof Error ? error.message : "Unknown error" },
@@ -5,7 +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
+ import { firstValueFrom, isError, isSnapshot } from "@sylphx/lens-core";
9
9
  import type { LensServer } from "../server/create.js";
10
10
 
11
11
  // =============================================================================
@@ -347,8 +347,8 @@ export function createHTTPHandler(
347
347
  }),
348
348
  );
349
349
 
350
- if (result.error) {
351
- return new Response(JSON.stringify({ error: sanitize(result.error) }), {
350
+ if (isError(result)) {
351
+ return new Response(JSON.stringify({ error: result.error }), {
352
352
  status: 500,
353
353
  headers: {
354
354
  "Content-Type": "application/json",
@@ -357,7 +357,17 @@ export function createHTTPHandler(
357
357
  });
358
358
  }
359
359
 
360
- return new Response(JSON.stringify({ data: result.data }), {
360
+ if (isSnapshot(result)) {
361
+ return new Response(JSON.stringify({ data: result.data }), {
362
+ headers: {
363
+ "Content-Type": "application/json",
364
+ ...baseHeaders,
365
+ },
366
+ });
367
+ }
368
+
369
+ // ops message - forward as-is
370
+ return new Response(JSON.stringify(result), {
361
371
  headers: {
362
372
  "Content-Type": "application/json",
363
373
  ...baseHeaders,
@@ -24,6 +24,8 @@
24
24
 
25
25
  import {
26
26
  firstValueFrom,
27
+ isError,
28
+ isSnapshot,
27
29
  type ReconnectMessage,
28
30
  type ReconnectSubscription,
29
31
  } from "@sylphx/lens-core";
@@ -272,20 +274,24 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
272
274
  }
273
275
 
274
276
  // Execute query first to get data
275
- let result: { data?: unknown; error?: Error };
277
+ let resultData: unknown;
276
278
  try {
277
- result = await firstValueFrom(server.execute({ path: operation, input }));
279
+ const result = await firstValueFrom(server.execute({ path: operation, input }));
278
280
 
279
- if (result.error) {
281
+ if (isError(result)) {
280
282
  conn.ws.send(
281
283
  JSON.stringify({
282
284
  type: "error",
283
285
  id,
284
- error: { code: "EXECUTION_ERROR", message: result.error.message },
286
+ error: { code: "EXECUTION_ERROR", message: result.error },
285
287
  }),
286
288
  );
287
289
  return;
288
290
  }
291
+
292
+ if (isSnapshot(result)) {
293
+ resultData = result.data;
294
+ }
289
295
  } catch (error) {
290
296
  conn.ws.send(
291
297
  JSON.stringify({
@@ -298,7 +304,7 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
298
304
  }
299
305
 
300
306
  // Extract entities from result
301
- const entities = result.data ? extractEntities(result.data) : [];
307
+ const entities = resultData ? extractEntities(resultData) : [];
302
308
 
303
309
  // Check for duplicate subscription ID - cleanup old one first
304
310
  const existingSub = conn.subscriptions.get(id);
@@ -329,7 +335,7 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
329
335
  fields,
330
336
  entityKeys: new Set(entities.map(({ entity, entityId }) => `${entity}:${entityId}`)),
331
337
  cleanups: [],
332
- lastData: result.data,
338
+ lastData: resultData,
333
339
  };
334
340
 
335
341
  // Register subscriptions with server for each entity
@@ -456,27 +462,29 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
456
462
  }),
457
463
  );
458
464
 
459
- if (result.error) {
465
+ if (isError(result)) {
460
466
  conn.ws.send(
461
467
  JSON.stringify({
462
468
  type: "error",
463
469
  id: message.id,
464
- error: { code: "EXECUTION_ERROR", message: result.error.message },
470
+ error: { code: "EXECUTION_ERROR", message: result.error },
465
471
  }),
466
472
  );
467
473
  return;
468
474
  }
469
475
 
470
- // Apply field selection if specified
471
- const selected = message.fields ? applySelection(result.data, message.fields) : result.data;
476
+ if (isSnapshot(result)) {
477
+ // Apply field selection if specified
478
+ const selected = message.fields ? applySelection(result.data, message.fields) : result.data;
472
479
 
473
- conn.ws.send(
474
- JSON.stringify({
475
- type: "result",
476
- id: message.id,
477
- data: selected,
478
- }),
479
- );
480
+ conn.ws.send(
481
+ JSON.stringify({
482
+ type: "result",
483
+ id: message.id,
484
+ data: selected,
485
+ }),
486
+ );
487
+ }
480
488
  } catch (error) {
481
489
  conn.ws.send(
482
490
  JSON.stringify({
@@ -498,32 +506,32 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
498
506
  }),
499
507
  );
500
508
 
501
- if (result.error) {
509
+ if (isError(result)) {
502
510
  conn.ws.send(
503
511
  JSON.stringify({
504
512
  type: "error",
505
513
  id: message.id,
506
- error: { code: "EXECUTION_ERROR", message: result.error.message },
514
+ error: { code: "EXECUTION_ERROR", message: result.error },
507
515
  }),
508
516
  );
509
517
  return;
510
518
  }
511
519
 
512
- // Broadcast to all subscribers of affected entities
513
- if (result.data) {
520
+ if (isSnapshot(result)) {
521
+ // Broadcast to all subscribers of affected entities
514
522
  const entities = extractEntities(result.data);
515
523
  for (const { entity, entityId, entityData } of entities) {
516
524
  await server.broadcast(entity, entityId, entityData);
517
525
  }
518
- }
519
526
 
520
- conn.ws.send(
521
- JSON.stringify({
522
- type: "result",
523
- id: message.id,
524
- data: result.data,
525
- }),
526
- );
527
+ conn.ws.send(
528
+ JSON.stringify({
529
+ type: "result",
530
+ id: message.id,
531
+ data: result.data,
532
+ }),
533
+ );
534
+ }
527
535
  } catch (error) {
528
536
  conn.ws.send(
529
537
  JSON.stringify({