@sylphx/lens-server 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,12 @@
1
1
  /**
2
2
  * @sylphx/lens-server - Handlers
3
3
  *
4
- * Protocol handlers for bridging the Lens app to various transports.
4
+ * Streaming handlers for real-time communication.
5
+ * HTTP is handled by app.fetch directly.
5
6
  */
6
7
 
7
8
  // =============================================================================
8
- // Unified Handler (HTTP + SSE)
9
- // =============================================================================
10
-
11
- export { createHandler, type Handler, type HandlerOptions } from "./unified.js";
12
-
13
- // =============================================================================
14
- // Framework Handler Utilities
9
+ // Framework Handler Utilities (for framework packages)
15
10
  // =============================================================================
16
11
 
17
12
  export {
@@ -34,18 +29,6 @@ export {
34
29
  type SSEHandlerConfig as SSEHandlerOptions,
35
30
  } from "../sse/handler.js";
36
31
 
37
- // =============================================================================
38
- // HTTP Handler
39
- // =============================================================================
40
-
41
- export {
42
- createHTTPHandler,
43
- type HealthCheckOptions,
44
- type HealthCheckResponse,
45
- type HTTPHandler,
46
- type HTTPHandlerOptions,
47
- } from "./http.js";
48
-
49
32
  // =============================================================================
50
33
  // WebSocket Handler
51
34
  // =============================================================================
@@ -55,7 +55,7 @@ function wait(ms = 10): Promise<void> {
55
55
  // =============================================================================
56
56
 
57
57
  const getUser = query()
58
- .input(z.object({ id: z.string() }))
58
+ .args(z.object({ id: z.string() }))
59
59
  .resolve(({ args }) => ({
60
60
  id: args.id,
61
61
  name: "Test User",
@@ -68,7 +68,7 @@ const listUsers = query().resolve(() => [
68
68
  ]);
69
69
 
70
70
  const createUser = mutation()
71
- .input(z.object({ name: z.string() }))
71
+ .args(z.object({ name: z.string() }))
72
72
  .resolve(({ args }) => ({
73
73
  id: "new-id",
74
74
  name: args.name,
@@ -76,7 +76,7 @@ const createUser = mutation()
76
76
  }));
77
77
 
78
78
  const slowQuery = query()
79
- .input(z.object({ delay: z.number() }))
79
+ .args(z.object({ delay: z.number() }))
80
80
  .resolve(async ({ args }) => {
81
81
  await new Promise((r) => setTimeout(r, args.delay));
82
82
  return { done: true };
package/src/index.ts CHANGED
@@ -3,33 +3,19 @@
3
3
  *
4
4
  * Server runtime for Lens API framework.
5
5
  *
6
- * Architecture:
7
- * - App = Executor with optional plugin support
8
- * - Stateless (default): Pure executor
9
- * - Stateful (with opLog): Cursor-based state synchronization
10
- * - Handlers = Pure protocol handlers (HTTP, WebSocket, SSE)
11
- * - No business logic - just translate protocol to app calls
12
- * - Plugins = App-level middleware (opLog, auth, logger)
13
- * - Configured at app level, not handler level
14
- *
15
6
  * @example
16
7
  * ```typescript
17
- * // Stateless mode (default)
18
- * const app = createApp({ router });
19
- * const wsHandler = createWSHandler(app);
20
- *
21
- * // With opLog plugin (cursor-based state sync)
22
8
  * const app = createApp({
23
- * router,
24
- * plugins: [opLog()],
25
- * });
9
+ * router: appRouter,
10
+ * entities: { User, Post },
11
+ * resolvers: [userResolver, postResolver],
12
+ * context: () => ({ db }),
13
+ * })
26
14
  *
27
- * // With external storage for serverless (install @sylphx/lens-storage-upstash)
28
- * import { upstashStorage } from "@sylphx/lens-storage-upstash";
29
- * const app = createApp({
30
- * router,
31
- * plugins: [opLog({ storage: upstashStorage({ redis }) })],
32
- * });
15
+ * // App is directly callable - works with any runtime
16
+ * Bun.serve({ fetch: app })
17
+ * Deno.serve(app)
18
+ * export default app // Cloudflare Workers
33
19
  * ```
34
20
  */
35
21
 
@@ -91,36 +77,34 @@ export {
91
77
  } from "./server/create.js";
92
78
 
93
79
  // =============================================================================
94
- // Protocol Handlers
80
+ // Streaming Handlers (WebSocket, SSE)
95
81
  // =============================================================================
96
82
 
97
83
  export {
98
- // Framework Handler Utilities
99
- createFrameworkHandler,
100
- // Unified Handler (HTTP + SSE)
101
- createHandler,
102
- // HTTP Handler
103
- createHTTPHandler,
104
- createServerClientProxy,
105
- // SSE Handler
84
+ // SSE Handler (for live queries)
106
85
  createSSEHandler,
107
- // WebSocket Handler
86
+ // WebSocket Handler (for live queries + subscriptions)
108
87
  createWSHandler,
109
88
  DEFAULT_WS_HANDLER_CONFIG,
110
- type FrameworkHandlerOptions,
111
- type Handler,
112
- type HandlerOptions,
113
- type HTTPHandler,
114
- type HTTPHandlerOptions,
115
- handleWebMutation,
116
- handleWebQuery,
117
- handleWebSSE,
118
89
  type SSEHandlerOptions,
119
90
  type WSHandler,
120
91
  type WSHandlerConfig,
121
92
  type WSHandlerOptions,
122
93
  } from "./handlers/index.js";
123
94
 
95
+ // =============================================================================
96
+ // Framework Integration Utilities (internal use)
97
+ // =============================================================================
98
+
99
+ export {
100
+ createFrameworkHandler,
101
+ createServerClientProxy,
102
+ type FrameworkHandlerOptions,
103
+ handleWebMutation,
104
+ handleWebQuery,
105
+ handleWebSSE,
106
+ } from "./handlers/index.js";
107
+
124
108
  // =============================================================================
125
109
  // Plugin System
126
110
  // =============================================================================
@@ -91,7 +91,7 @@ const User = model("User", {
91
91
  // =============================================================================
92
92
 
93
93
  const getUser = query()
94
- .input(z.object({ id: z.string() }))
94
+ .args(z.object({ id: z.string() }))
95
95
  .returns(User)
96
96
  .resolve(({ args }) => ({
97
97
  id: args.id,
@@ -105,7 +105,7 @@ const getUsers = query().resolve(() => [
105
105
  ]);
106
106
 
107
107
  const createUser = mutation()
108
- .input(z.object({ name: z.string(), email: z.string().optional() }))
108
+ .args(z.object({ name: z.string(), email: z.string().optional() }))
109
109
  .returns(User)
110
110
  .resolve(({ args }) => ({
111
111
  id: "new-id",
@@ -114,7 +114,7 @@ const createUser = mutation()
114
114
  }));
115
115
 
116
116
  const updateUser = mutation()
117
- .input(z.object({ id: z.string(), name: z.string().optional() }))
117
+ .args(z.object({ id: z.string(), name: z.string().optional() }))
118
118
  .returns(User)
119
119
  .resolve(({ args }) => ({
120
120
  id: args.id,
@@ -122,7 +122,7 @@ const updateUser = mutation()
122
122
  }));
123
123
 
124
124
  const deleteUser = mutation()
125
- .input(z.object({ id: z.string() }))
125
+ .args(z.object({ id: z.string() }))
126
126
  .resolve(() => ({ success: true }));
127
127
 
128
128
  // =============================================================================
@@ -374,7 +374,7 @@ describe("execute", () => {
374
374
 
375
375
  it("handles resolver errors gracefully", async () => {
376
376
  const errorQuery = query()
377
- .input(z.object({ id: z.string() }))
377
+ .args(z.object({ id: z.string() }))
378
378
  .resolve(() => {
379
379
  throw new Error("Resolver error");
380
380
  });
@@ -423,7 +423,7 @@ describe("context", () => {
423
423
  let capturedContext: unknown = null;
424
424
 
425
425
  const contextQuery = query()
426
- .input(z.object({ id: z.string() }))
426
+ .args(z.object({ id: z.string() }))
427
427
  .resolve(({ ctx }) => {
428
428
  capturedContext = ctx;
429
429
  return { id: "1", name: "test" };
@@ -451,7 +451,7 @@ describe("context", () => {
451
451
  let capturedContext: unknown = null;
452
452
 
453
453
  const contextQuery = query()
454
- .input(z.object({ id: z.string() }))
454
+ .args(z.object({ id: z.string() }))
455
455
  .resolve(({ ctx }) => {
456
456
  capturedContext = ctx;
457
457
  return { id: "1", name: "test" };
@@ -590,7 +590,7 @@ describe("field resolvers", () => {
590
590
  }));
591
591
 
592
592
  const getAuthor = query<TestContext>()
593
- .input(z.object({ id: z.string() }))
593
+ .args(z.object({ id: z.string() }))
594
594
  .returns(Author)
595
595
  .resolve(({ args, ctx }) => {
596
596
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -649,7 +649,7 @@ describe("field resolvers", () => {
649
649
  }));
650
650
 
651
651
  const getAuthor = query<TestContext>()
652
- .input(z.object({ id: z.string() }))
652
+ .args(z.object({ id: z.string() }))
653
653
  .returns(Author)
654
654
  .resolve(({ args, ctx }) => {
655
655
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -743,7 +743,7 @@ describe("field resolvers", () => {
743
743
  }));
744
744
 
745
745
  const getAuthor = query<CtxWithComments>()
746
- .input(z.object({ id: z.string() }))
746
+ .args(z.object({ id: z.string() }))
747
747
  .returns(LocalAuthor)
748
748
  .resolve(({ args, ctx }) => {
749
749
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -799,7 +799,7 @@ describe("field resolvers", () => {
799
799
  }));
800
800
 
801
801
  const getAuthor = query<TestContext>()
802
- .input(z.object({ id: z.string() }))
802
+ .args(z.object({ id: z.string() }))
803
803
  .returns(Author)
804
804
  .resolve(({ args, ctx }) => {
805
805
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -872,7 +872,7 @@ describe("field resolvers", () => {
872
872
 
873
873
  // Use .resolve().subscribe() pattern - emit comes through Publisher callback, NOT ctx
874
874
  const getAuthor = query<{ db: typeof mockDb }>()
875
- .input(z.object({ id: z.string() }))
875
+ .args(z.object({ id: z.string() }))
876
876
  .returns(Author)
877
877
  .resolve(({ args, ctx }) => {
878
878
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -981,7 +981,7 @@ describe("field resolvers", () => {
981
981
  }));
982
982
 
983
983
  const getAuthor = query<{ db: typeof mockDb }>()
984
- .input(z.object({ id: z.string() }))
984
+ .args(z.object({ id: z.string() }))
985
985
  .returns(Author)
986
986
  .resolve(({ args, ctx }) => {
987
987
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -1070,7 +1070,7 @@ describe("field resolvers", () => {
1070
1070
  }));
1071
1071
 
1072
1072
  const getAuthor = query<{ db: typeof mockDb }>()
1073
- .input(z.object({ id: z.string() }))
1073
+ .args(z.object({ id: z.string() }))
1074
1074
  .returns(Author)
1075
1075
  .resolve(({ args, ctx }) => {
1076
1076
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -1146,7 +1146,7 @@ describe("field resolvers", () => {
1146
1146
 
1147
1147
  // Use .resolve().subscribe() pattern - emit comes through Publisher callback
1148
1148
  const liveQuery = query()
1149
- .input(z.object({ id: z.string() }))
1149
+ .args(z.object({ id: z.string() }))
1150
1150
  .returns(User)
1151
1151
  .resolve(({ args }) => {
1152
1152
  return { id: args.id, name: "Initial" };
@@ -1244,7 +1244,7 @@ describe("field resolvers", () => {
1244
1244
  }));
1245
1245
 
1246
1246
  const getAuthor = query<{ db: typeof mockDb }>()
1247
- .input(z.object({ id: z.string() }))
1247
+ .args(z.object({ id: z.string() }))
1248
1248
  .returns(Author)
1249
1249
  .resolve(({ args, ctx }) => {
1250
1250
  const author = ctx.db.authors.find((a) => a.id === args.id);
@@ -1324,7 +1324,7 @@ describe("field resolvers", () => {
1324
1324
  describe("observable behavior", () => {
1325
1325
  it("delivers initial result immediately for queries", async () => {
1326
1326
  const simpleQuery = query()
1327
- .input(z.object({ id: z.string() }))
1327
+ .args(z.object({ id: z.string() }))
1328
1328
  .resolve(({ args }) => ({ id: args.id, name: "Test" }));
1329
1329
 
1330
1330
  const server = createApp({ queries: { simpleQuery } });
@@ -1348,7 +1348,7 @@ describe("observable behavior", () => {
1348
1348
 
1349
1349
  // Use .resolve().subscribe() pattern - emit comes through Publisher callback
1350
1350
  const liveQuery = query()
1351
- .input(z.object({ id: z.string() }))
1351
+ .args(z.object({ id: z.string() }))
1352
1352
  .resolve(({ args }) => {
1353
1353
  return { id: args.id, name: "Initial" };
1354
1354
  })
@@ -1381,7 +1381,7 @@ describe("observable behavior", () => {
1381
1381
 
1382
1382
  it("delivers mutation result via observable", async () => {
1383
1383
  const testMutation = mutation()
1384
- .input(z.object({ name: z.string() }))
1384
+ .args(z.object({ name: z.string() }))
1385
1385
  .resolve(({ args }) => ({ id: "new", name: args.name }));
1386
1386
 
1387
1387
  const server = createApp({ mutations: { testMutation } });
@@ -1403,7 +1403,7 @@ describe("observable behavior", () => {
1403
1403
  let resolverCalls = 0;
1404
1404
 
1405
1405
  const simpleQuery = query()
1406
- .input(z.object({ id: z.string() }))
1406
+ .args(z.object({ id: z.string() }))
1407
1407
  .resolve(({ args }) => {
1408
1408
  resolverCalls++;
1409
1409
  return { id: args.id };
@@ -1438,7 +1438,7 @@ describe("emit backpressure", () => {
1438
1438
 
1439
1439
  // Use .resolve().subscribe() pattern - emit comes through Publisher callback
1440
1440
  const liveQuery = query()
1441
- .input(z.object({ id: z.string() }))
1441
+ .args(z.object({ id: z.string() }))
1442
1442
  .resolve(({ args }) => {
1443
1443
  return { id: args.id, count: 0 };
1444
1444
  })
@@ -1485,7 +1485,7 @@ describe("emit backpressure", () => {
1485
1485
  describe("observable error handling", () => {
1486
1486
  it("propagates resolver errors to observer", async () => {
1487
1487
  const errorQuery = query()
1488
- .input(z.object({ id: z.string() }))
1488
+ .args(z.object({ id: z.string() }))
1489
1489
  .resolve(() => {
1490
1490
  throw new Error("Test error");
1491
1491
  });
@@ -1507,7 +1507,7 @@ describe("observable error handling", () => {
1507
1507
 
1508
1508
  it("handles async resolver errors", async () => {
1509
1509
  const asyncErrorQuery = query()
1510
- .input(z.object({ id: z.string() }))
1510
+ .args(z.object({ id: z.string() }))
1511
1511
  .resolve(async () => {
1512
1512
  await new Promise((r) => setTimeout(r, 10));
1513
1513
  throw new Error("Async error");
@@ -1546,7 +1546,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1546
1546
  let capturedOnCleanup: ((fn: () => void) => void) | undefined;
1547
1547
 
1548
1548
  const liveUser = query()
1549
- .input(z.object({ id: z.string() }))
1549
+ .args(z.object({ id: z.string() }))
1550
1550
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1551
1551
  .subscribe(({ args: _args }) => ({ emit, onCleanup }) => {
1552
1552
  subscriberCalled = true;
@@ -1586,7 +1586,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1586
1586
  let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
1587
1587
 
1588
1588
  const liveUser = query()
1589
- .input(z.object({ id: z.string() }))
1589
+ .args(z.object({ id: z.string() }))
1590
1590
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1591
1591
  .subscribe(() => ({ emit }) => {
1592
1592
  capturedEmit = emit;
@@ -1635,7 +1635,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1635
1635
  let cleanupCalled = false;
1636
1636
 
1637
1637
  const liveUser = query()
1638
- .input(z.object({ id: z.string() }))
1638
+ .args(z.object({ id: z.string() }))
1639
1639
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1640
1640
  .subscribe(() => ({ onCleanup }) => {
1641
1641
  onCleanup(() => {
@@ -1672,7 +1672,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1672
1672
  let receivedCtx: TestContext | undefined;
1673
1673
 
1674
1674
  const liveUser = query<TestContext>()
1675
- .input(z.object({ id: z.string() }))
1675
+ .args(z.object({ id: z.string() }))
1676
1676
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1677
1677
  .subscribe(({ args, ctx }) => ({ emit: _emit }) => {
1678
1678
  receivedInput = args;
@@ -1702,7 +1702,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1702
1702
 
1703
1703
  it("handles subscriber errors gracefully", async () => {
1704
1704
  const liveUser = query()
1705
- .input(z.object({ id: z.string() }))
1705
+ .args(z.object({ id: z.string() }))
1706
1706
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1707
1707
  .subscribe(() => () => {
1708
1708
  throw new Error("Subscriber error");
@@ -1745,7 +1745,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1745
1745
  let capturedEmit: ((value: { id: string; count: number }) => void) | undefined;
1746
1746
 
1747
1747
  const liveCounter = query()
1748
- .input(z.object({ id: z.string() }))
1748
+ .args(z.object({ id: z.string() }))
1749
1749
  .resolve(({ args }) => ({ id: args.id, count: 0 }))
1750
1750
  .subscribe(() => ({ emit }) => {
1751
1751
  capturedEmit = emit;
@@ -1797,7 +1797,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1797
1797
  let capturedEmit: EmitFn | undefined;
1798
1798
 
1799
1799
  const liveUser = query()
1800
- .input(z.object({ id: z.string() }))
1800
+ .args(z.object({ id: z.string() }))
1801
1801
  .resolve(({ args }) => ({ id: args.id, name: "Initial", status: "offline" }))
1802
1802
  .subscribe(() => ({ emit }) => {
1803
1803
  capturedEmit = emit as EmitFn;
@@ -1844,7 +1844,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1844
1844
  let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
1845
1845
 
1846
1846
  const liveUser = query()
1847
- .input(z.object({ id: z.string() }))
1847
+ .args(z.object({ id: z.string() }))
1848
1848
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1849
1849
  .subscribe(() => ({ emit }) => {
1850
1850
  capturedEmit = emit;
@@ -1892,7 +1892,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1892
1892
  const emits: Array<(value: { id: string; name: string }) => void> = [];
1893
1893
 
1894
1894
  const liveUser = query()
1895
- .input(z.object({ id: z.string() }))
1895
+ .args(z.object({ id: z.string() }))
1896
1896
  .resolve(({ args }) => ({ id: args.id, name: "Initial" }))
1897
1897
  .subscribe(() => ({ emit }) => {
1898
1898
  subscriberCallCount++;
@@ -1967,7 +1967,7 @@ describe("scalar field subscription with emit.delta()", () => {
1967
1967
  entities: { UserWithBio },
1968
1968
  queries: {
1969
1969
  getUserWithBio: query()
1970
- .input(z.object({ id: z.string() }))
1970
+ .args(z.object({ id: z.string() }))
1971
1971
  .returns(UserWithBio)
1972
1972
  .resolve(({ args }) => ({ id: args.id, name: "Alice" })),
1973
1973
  },
@@ -2030,7 +2030,7 @@ describe("scalar field subscription with emit.delta()", () => {
2030
2030
  entities: { UserWithContent },
2031
2031
  queries: {
2032
2032
  getUserWithContent: query()
2033
- .input(z.object({ id: z.string() }))
2033
+ .args(z.object({ id: z.string() }))
2034
2034
  .returns(UserWithContent)
2035
2035
  .resolve(({ args }) => ({ id: args.id, name: "Alice" })),
2036
2036
  },
@@ -23,13 +23,16 @@ import {
23
23
  createResolverFromEntity,
24
24
  type Emit,
25
25
  type EmitCommand,
26
+ firstValueFrom,
26
27
  flattenRouter,
27
28
  hashValue,
28
29
  type InferRouterContext,
30
+ isError,
29
31
  isLiveQueryDef,
30
32
  isModelDef,
31
33
  isMutationDef,
32
34
  isQueryDef,
35
+ isSnapshot,
33
36
  isSubscriptionDef,
34
37
  type LiveQueryDef,
35
38
  type Message,
@@ -114,8 +117,7 @@ class LensServerImpl<
114
117
  M extends MutationsMap = MutationsMap,
115
118
  S extends SubscriptionsMap = SubscriptionsMap,
116
119
  TContext extends ContextValue = ContextValue,
117
- > implements LensServer
118
- {
120
+ > {
119
121
  private queries: Q;
120
122
  private mutations: M;
121
123
  private subscriptions: S;
@@ -420,7 +422,7 @@ class LensServerImpl<
420
422
  }
421
423
 
422
424
  // Get the publisher function
423
- const publisher = subscriber({ input, ctx: context });
425
+ const publisher = subscriber({ args: input, ctx: context });
424
426
 
425
427
  // Call publisher with emit/onCleanup callbacks
426
428
  if (publisher) {
@@ -1220,6 +1222,112 @@ class LensServerImpl<
1220
1222
  getPluginManager(): PluginManager {
1221
1223
  return this.pluginManager;
1222
1224
  }
1225
+
1226
+ // =========================================================================
1227
+ // HTTP Fetch Handler
1228
+ // =========================================================================
1229
+
1230
+ /**
1231
+ * HTTP fetch handler - Web standard Request/Response.
1232
+ * Works with any runtime: Bun, Deno, Cloudflare Workers, etc.
1233
+ */
1234
+ fetch = async (request: Request): Promise<Response> => {
1235
+ const url = new URL(request.url);
1236
+ const pathname = url.pathname;
1237
+
1238
+ // Base headers including CORS and security
1239
+ const baseHeaders: Record<string, string> = {
1240
+ "Content-Type": "application/json",
1241
+ "X-Content-Type-Options": "nosniff",
1242
+ "X-Frame-Options": "DENY",
1243
+ "Access-Control-Allow-Origin": "*",
1244
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
1245
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
1246
+ };
1247
+
1248
+ // Handle CORS preflight
1249
+ if (request.method === "OPTIONS") {
1250
+ return new Response(null, { status: 204, headers: baseHeaders });
1251
+ }
1252
+
1253
+ // Health check: GET /__lens/health
1254
+ if (request.method === "GET" && pathname === "/__lens/health") {
1255
+ const metadata = this.getMetadata();
1256
+ return new Response(
1257
+ JSON.stringify({
1258
+ status: "healthy",
1259
+ service: "lens-server",
1260
+ version: metadata.version,
1261
+ timestamp: new Date().toISOString(),
1262
+ }),
1263
+ {
1264
+ headers: {
1265
+ ...baseHeaders,
1266
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1267
+ },
1268
+ },
1269
+ );
1270
+ }
1271
+
1272
+ // Metadata: GET /__lens/metadata
1273
+ if (request.method === "GET" && pathname === "/__lens/metadata") {
1274
+ return new Response(JSON.stringify(this.getMetadata()), {
1275
+ headers: baseHeaders,
1276
+ });
1277
+ }
1278
+
1279
+ // Operations: POST /
1280
+ if (request.method === "POST" && (pathname === "/" || pathname === "")) {
1281
+ let body: { path?: string; input?: unknown };
1282
+ try {
1283
+ body = (await request.json()) as typeof body;
1284
+ } catch {
1285
+ return new Response(JSON.stringify({ error: "Invalid JSON in request body" }), {
1286
+ status: 400,
1287
+ headers: baseHeaders,
1288
+ });
1289
+ }
1290
+
1291
+ if (!body.path) {
1292
+ return new Response(JSON.stringify({ error: "Missing operation path" }), {
1293
+ status: 400,
1294
+ headers: baseHeaders,
1295
+ });
1296
+ }
1297
+
1298
+ try {
1299
+ const result = await firstValueFrom(this.execute({ path: body.path, input: body.input }));
1300
+
1301
+ if (isError(result)) {
1302
+ return new Response(JSON.stringify({ error: result.error }), {
1303
+ status: 500,
1304
+ headers: baseHeaders,
1305
+ });
1306
+ }
1307
+
1308
+ if (isSnapshot(result)) {
1309
+ return new Response(JSON.stringify({ data: result.data }), {
1310
+ headers: baseHeaders,
1311
+ });
1312
+ }
1313
+
1314
+ // ops message - forward as-is
1315
+ return new Response(JSON.stringify(result), { headers: baseHeaders });
1316
+ } catch (error) {
1317
+ const errMsg = error instanceof Error ? error.message : String(error);
1318
+ return new Response(JSON.stringify({ error: errMsg }), {
1319
+ status: 500,
1320
+ headers: baseHeaders,
1321
+ });
1322
+ }
1323
+ }
1324
+
1325
+ // Not found
1326
+ return new Response(JSON.stringify({ error: "Not found" }), {
1327
+ status: 404,
1328
+ headers: baseHeaders,
1329
+ });
1330
+ };
1223
1331
  }
1224
1332
 
1225
1333
  // =============================================================================
@@ -1227,20 +1335,31 @@ class LensServerImpl<
1227
1335
  // =============================================================================
1228
1336
 
1229
1337
  /**
1230
- * Create Lens server with optional plugin support.
1338
+ * Create Lens app - a callable HTTP handler.
1339
+ *
1340
+ * The returned app is directly usable as a fetch handler.
1341
+ * Works with any runtime: Bun, Deno, Cloudflare Workers, Node.js.
1231
1342
  *
1232
1343
  * @example
1233
1344
  * ```typescript
1234
- * // Stateless mode (default)
1235
- * const app = createApp({ router });
1236
- * createWSHandler(app); // Sends full data on each update
1237
- *
1238
- * // Stateful mode (with clientState)
1239
1345
  * const app = createApp({
1240
- * router,
1241
- * plugins: [clientState()], // Enables per-client state tracking
1242
- * });
1243
- * createWSHandler(app); // Sends minimal diffs
1346
+ * router: appRouter,
1347
+ * entities: { User, Post },
1348
+ * resolvers: [userResolver, postResolver],
1349
+ * context: () => ({ db }),
1350
+ * })
1351
+ *
1352
+ * // Bun - app is directly callable
1353
+ * Bun.serve(app)
1354
+ *
1355
+ * // Deno
1356
+ * Deno.serve(app)
1357
+ *
1358
+ * // Cloudflare Workers
1359
+ * export default app
1360
+ *
1361
+ * // Or use app.fetch explicitly
1362
+ * Bun.serve({ fetch: app.fetch })
1244
1363
  * ```
1245
1364
  */
1246
1365
  export function createApp<
@@ -1269,7 +1388,17 @@ export function createApp<
1269
1388
  config: LensServerConfig<TContext> & { queries?: Q; mutations?: M },
1270
1389
  ): LensServer & { _types: { queries: Q; mutations: M; context: TContext } } {
1271
1390
  const server = new LensServerImpl(config) as LensServerImpl<Q, M, SubscriptionsMap, TContext>;
1272
- return server as unknown as LensServer & {
1391
+
1392
+ // Create callable function that delegates to fetch
1393
+ const app = ((request: Request) => server.fetch(request)) as LensServer & {
1273
1394
  _types: { queries: Q; mutations: M; context: TContext };
1274
1395
  };
1396
+
1397
+ // Attach properties
1398
+ app.fetch = server.fetch;
1399
+ app.execute = server.execute.bind(server);
1400
+ app.getMetadata = server.getMetadata.bind(server);
1401
+ app.getPluginManager = server.getPluginManager.bind(server);
1402
+
1403
+ return app;
1275
1404
  }