@sylphx/lens-server 2.13.2 → 2.14.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.
@@ -3,14 +3,75 @@
3
3
  *
4
4
  * Tests for the pure executor server.
5
5
  * Server only does: getMetadata() and execute()
6
+ *
7
+ * STATELESS ARCHITECTURE:
8
+ * - Server sends initial data via { $: "snapshot", data }
9
+ * - Server sends updates via { $: "ops", ops: Op[] }
10
+ * - Client applies updates to local state using applyOps
6
11
  */
7
12
 
8
13
  import { describe, expect, it } from "bun:test";
9
- import { entity, firstValueFrom, mutation, query, resolver, router, t } from "@sylphx/lens-core";
14
+ import {
15
+ applyOps,
16
+ entity,
17
+ firstValueFrom,
18
+ isError,
19
+ isOps,
20
+ isSnapshot,
21
+ type Message,
22
+ mutation,
23
+ query,
24
+ resolver,
25
+ router,
26
+ t,
27
+ } from "@sylphx/lens-core";
10
28
  import { z } from "zod";
11
29
  import { optimisticPlugin } from "../plugin/optimistic.js";
12
30
  import { createApp } from "./create.js";
13
31
 
32
+ // =============================================================================
33
+ // Test Helpers for Stateless Architecture
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Create a results collector that simulates client behavior.
38
+ * Maintains current state by applying incoming Message updates.
39
+ *
40
+ * NEW PROTOCOL:
41
+ * - { $: "snapshot", data } → replace current state
42
+ * - { $: "ops", ops: Op[] } → apply operations to current state
43
+ * - { $: "error", error } → error message
44
+ */
45
+ function createResultsCollector() {
46
+ let currentData: unknown = null;
47
+ const rawResults: Message[] = [];
48
+
49
+ return {
50
+ /** Raw results (Message format) */
51
+ get raw() {
52
+ return rawResults;
53
+ },
54
+ /** Current accumulated state */
55
+ get current() {
56
+ return currentData;
57
+ },
58
+ /** Number of ops messages received */
59
+ get updateCount() {
60
+ return rawResults.filter((r) => isOps(r)).length;
61
+ },
62
+ /** Push a Message result (applies ops if present) */
63
+ push(result: Message) {
64
+ rawResults.push(result);
65
+ if (isSnapshot(result)) {
66
+ currentData = result.data;
67
+ }
68
+ if (isOps(result)) {
69
+ currentData = applyOps(currentData, result.ops);
70
+ }
71
+ },
72
+ };
73
+ }
74
+
14
75
  // =============================================================================
15
76
  // Test Entities
16
77
  // =============================================================================
@@ -199,12 +260,14 @@ describe("execute", () => {
199
260
  }),
200
261
  );
201
262
 
202
- expect(result.data).toEqual({
203
- id: "123",
204
- name: "Test User",
205
- email: "test@example.com",
206
- });
207
- expect(result.error).toBeUndefined();
263
+ expect(isSnapshot(result)).toBe(true);
264
+ if (isSnapshot(result)) {
265
+ expect(result.data).toEqual({
266
+ id: "123",
267
+ name: "Test User",
268
+ email: "test@example.com",
269
+ });
270
+ }
208
271
  });
209
272
 
210
273
  it("executes mutation successfully", async () => {
@@ -219,12 +282,14 @@ describe("execute", () => {
219
282
  }),
220
283
  );
221
284
 
222
- expect(result.data).toEqual({
223
- id: "new-id",
224
- name: "New User",
225
- email: "new@example.com",
226
- });
227
- expect(result.error).toBeUndefined();
285
+ expect(isSnapshot(result)).toBe(true);
286
+ if (isSnapshot(result)) {
287
+ expect(result.data).toEqual({
288
+ id: "new-id",
289
+ name: "New User",
290
+ email: "new@example.com",
291
+ });
292
+ }
228
293
  });
229
294
 
230
295
  it("returns error for unknown operation", async () => {
@@ -239,9 +304,10 @@ describe("execute", () => {
239
304
  }),
240
305
  );
241
306
 
242
- expect(result.data).toBeUndefined();
243
- expect(result.error).toBeInstanceOf(Error);
244
- expect(result.error?.message).toContain("not found");
307
+ expect(isError(result)).toBe(true);
308
+ if (isError(result)) {
309
+ expect(result.error).toContain("not found");
310
+ }
245
311
  });
246
312
 
247
313
  it("returns error for invalid input", async () => {
@@ -256,8 +322,7 @@ describe("execute", () => {
256
322
  }),
257
323
  );
258
324
 
259
- expect(result.data).toBeUndefined();
260
- expect(result.error).toBeInstanceOf(Error);
325
+ expect(isError(result)).toBe(true);
261
326
  });
262
327
 
263
328
  it("executes router operations with dot notation", async () => {
@@ -277,11 +342,14 @@ describe("execute", () => {
277
342
  }),
278
343
  );
279
344
 
280
- expect(queryResult.data).toEqual({
281
- id: "456",
282
- name: "Test User",
283
- email: "test@example.com",
284
- });
345
+ expect(isSnapshot(queryResult)).toBe(true);
346
+ if (isSnapshot(queryResult)) {
347
+ expect(queryResult.data).toEqual({
348
+ id: "456",
349
+ name: "Test User",
350
+ email: "test@example.com",
351
+ });
352
+ }
285
353
 
286
354
  const mutationResult = await firstValueFrom(
287
355
  server.execute({
@@ -290,11 +358,14 @@ describe("execute", () => {
290
358
  }),
291
359
  );
292
360
 
293
- expect(mutationResult.data).toEqual({
294
- id: "new-id",
295
- name: "Router User",
296
- email: undefined,
297
- });
361
+ expect(isSnapshot(mutationResult)).toBe(true);
362
+ if (isSnapshot(mutationResult)) {
363
+ expect(mutationResult.data).toEqual({
364
+ id: "new-id",
365
+ name: "Router User",
366
+ email: undefined,
367
+ });
368
+ }
298
369
  });
299
370
 
300
371
  it("handles resolver errors gracefully", async () => {
@@ -315,9 +386,10 @@ describe("execute", () => {
315
386
  }),
316
387
  );
317
388
 
318
- expect(result.data).toBeUndefined();
319
- expect(result.error).toBeInstanceOf(Error);
320
- expect(result.error?.message).toBe("Resolver error");
389
+ expect(isError(result)).toBe(true);
390
+ if (isError(result)) {
391
+ expect(result.error).toBe("Resolver error");
392
+ }
321
393
  });
322
394
 
323
395
  it("executes query without input", async () => {
@@ -331,7 +403,10 @@ describe("execute", () => {
331
403
  }),
332
404
  );
333
405
 
334
- expect(result.data).toHaveLength(2);
406
+ expect(isSnapshot(result)).toBe(true);
407
+ if (isSnapshot(result)) {
408
+ expect(result.data).toHaveLength(2);
409
+ }
335
410
  });
336
411
  });
337
412
 
@@ -419,11 +494,14 @@ describe("selection", () => {
419
494
  }),
420
495
  );
421
496
 
422
- expect(result.data).toEqual({
423
- id: "123", // id always included
424
- name: "Test User",
425
- });
426
- expect((result.data as Record<string, unknown>).email).toBeUndefined();
497
+ expect(isSnapshot(result)).toBe(true);
498
+ if (isSnapshot(result)) {
499
+ expect(result.data).toEqual({
500
+ id: "123", // id always included
501
+ name: "Test User",
502
+ });
503
+ expect((result.data as Record<string, unknown>).email).toBeUndefined();
504
+ }
427
505
  });
428
506
  });
429
507
 
@@ -541,14 +619,16 @@ describe("field resolvers", () => {
541
619
  }),
542
620
  );
543
621
 
544
- expect(result.error).toBeUndefined();
545
- expect(result.data).toBeDefined();
622
+ expect(isSnapshot(result)).toBe(true);
623
+ if (isSnapshot(result)) {
624
+ expect(result.data).toBeDefined();
546
625
 
547
- const data = result.data as { id: string; name: string; posts: { id: string; title: string }[] };
548
- expect(data.id).toBe("a1");
549
- expect(data.name).toBe("Alice");
550
- expect(data.posts).toHaveLength(2); // limit: 2
551
- expect(data.posts.every((p) => p.title)).toBe(true); // only selected fields
626
+ const data = result.data as { id: string; name: string; posts: { id: string; title: string }[] };
627
+ expect(data.id).toBe("a1");
628
+ expect(data.name).toBe("Alice");
629
+ expect(data.posts).toHaveLength(2); // limit: 2
630
+ expect(data.posts.every((p) => p.title)).toBe(true); // only selected fields
631
+ }
552
632
  });
553
633
 
554
634
  it("passes context to field resolvers", async () => {
@@ -739,8 +819,9 @@ describe("field resolvers", () => {
739
819
  expect(data.posts).toHaveLength(3); // All of Alice's posts (default limit 10)
740
820
  });
741
821
 
742
- it("emit() goes through field resolvers", async () => {
743
- // Track how many times posts resolver is called
822
+ it("emit() sends update command (stateless architecture)", async () => {
823
+ // STATELESS: Field resolvers only run on initial resolve.
824
+ // Emits forward commands to client - no re-resolution on server.
744
825
  let postsResolverCallCount = 0;
745
826
 
746
827
  const Author = entity("Author", {
@@ -792,7 +873,7 @@ describe("field resolvers", () => {
792
873
  });
793
874
 
794
875
  // Subscribe to query with nested posts
795
- const results: unknown[] = [];
876
+ const collector = createResultsCollector();
796
877
  const subscription = server
797
878
  .execute({
798
879
  path: "getAuthor",
@@ -807,31 +888,34 @@ describe("field resolvers", () => {
807
888
  })
808
889
  .subscribe({
809
890
  next: (result) => {
810
- results.push(result);
891
+ collector.push(result as Message);
811
892
  },
812
893
  });
813
894
 
814
895
  // Wait for initial result
815
896
  await new Promise((resolve) => setTimeout(resolve, 50));
816
897
 
817
- expect(results.length).toBe(1);
898
+ expect(collector.raw.length).toBe(1);
818
899
  expect(postsResolverCallCount).toBe(1); // Called once for initial query
819
900
 
820
- // Add a new post to the mock DB
821
- mockDb.posts.push({ id: "p3", title: "Post 3", authorId: "a1" });
822
-
823
- // Emit updated author (this should trigger field resolvers)
901
+ // Emit updated author
824
902
  capturedEmit!({ id: "a1", name: "Alice Updated" });
825
903
 
826
904
  // Wait for emit to process
827
905
  await new Promise((resolve) => setTimeout(resolve, 50));
828
906
 
829
- expect(results.length).toBe(2);
830
- expect(postsResolverCallCount).toBe(2); // Called again after emit!
907
+ // STATELESS: Server sends ops command, not re-resolved data
908
+ expect(collector.raw.length).toBe(2);
909
+ expect(postsResolverCallCount).toBe(1); // NOT called again - stateless!
831
910
 
832
- // Verify the emitted result includes the new post (from re-running posts resolver)
833
- const latestResult = results[1] as { data: { posts: { id: string }[] } };
834
- expect(latestResult.data.posts).toHaveLength(3); // Should have 3 posts now
911
+ // Second result should be an ops command
912
+ const updateResult = collector.raw[1];
913
+ expect(isOps(updateResult)).toBe(true);
914
+
915
+ // Client applies update: name is updated, posts preserved from initial
916
+ const currentState = collector.current as { name: string; posts: { id: string }[] };
917
+ expect(currentState.name).toBe("Alice Updated");
918
+ expect(currentState.posts).toHaveLength(2); // Original 2 posts preserved
835
919
 
836
920
  subscription.unsubscribe();
837
921
  });
@@ -922,7 +1006,8 @@ describe("field resolvers", () => {
922
1006
  expect(cleanupCalled).toBe(true);
923
1007
  });
924
1008
 
925
- it("field-level emit updates specific field and notifies observer", async () => {
1009
+ it("field-level emit sends update command (stateless)", async () => {
1010
+ // STATELESS: Field emit sends update command with field path prefix
926
1011
  const Author = entity("Author", {
927
1012
  id: t.id(),
928
1013
  name: t.string(),
@@ -984,7 +1069,7 @@ describe("field resolvers", () => {
984
1069
  context: () => ({ db: mockDb }),
985
1070
  });
986
1071
 
987
- const results: unknown[] = [];
1072
+ const collector = createResultsCollector();
988
1073
  const subscription = server
989
1074
  .execute({
990
1075
  path: "getAuthor",
@@ -999,18 +1084,21 @@ describe("field resolvers", () => {
999
1084
  })
1000
1085
  .subscribe({
1001
1086
  next: (result) => {
1002
- results.push(result);
1087
+ collector.push(result as Message);
1003
1088
  },
1004
1089
  });
1005
1090
 
1006
1091
  // Wait for initial result
1007
1092
  await new Promise((resolve) => setTimeout(resolve, 50));
1008
1093
 
1009
- expect(results.length).toBe(1);
1094
+ expect(collector.raw.length).toBe(1);
1010
1095
  expect(capturedFieldEmit).toBeDefined();
1011
1096
 
1012
- const initialResult = results[0] as { data: { posts: { id: string }[] } };
1013
- expect(initialResult.data.posts).toHaveLength(2);
1097
+ const initialResult = collector.raw[0];
1098
+ expect(isSnapshot(initialResult)).toBe(true);
1099
+ if (isSnapshot(initialResult)) {
1100
+ expect((initialResult.data as { posts: { id: string }[] }).posts).toHaveLength(2);
1101
+ }
1014
1102
 
1015
1103
  // Use field-level emit to update just the posts field
1016
1104
  const newPosts = [
@@ -1023,15 +1111,20 @@ describe("field resolvers", () => {
1023
1111
  // Wait for field emit to process
1024
1112
  await new Promise((resolve) => setTimeout(resolve, 50));
1025
1113
 
1026
- expect(results.length).toBe(2);
1027
- const updatedResult = results[1] as { data: { posts: { id: string; title: string }[] } };
1028
- expect(updatedResult.data.posts).toHaveLength(3);
1029
- expect(updatedResult.data.posts[2].title).toBe("New Post 3");
1114
+ // STATELESS: Second result should be ops command
1115
+ expect(collector.raw.length).toBe(2);
1116
+ expect(isOps(collector.raw[1])).toBe(true);
1117
+
1118
+ // Client applies update to get final state
1119
+ const currentState = collector.current as { posts: { id: string; title: string }[] };
1120
+ expect(currentState.posts).toHaveLength(3);
1121
+ expect(currentState.posts[2].title).toBe("New Post 3");
1030
1122
 
1031
1123
  subscription.unsubscribe();
1032
1124
  });
1033
1125
 
1034
- it("emit skips observer notification when value unchanged", async () => {
1126
+ it("stateless server forwards all emit commands (no deduplication)", async () => {
1127
+ // STATELESS: Server forwards all commands. Deduplication is client/plugin responsibility.
1035
1128
  type EmitFn = (data: { id: string; name: string }) => void;
1036
1129
  let capturedEmit: EmitFn | undefined;
1037
1130
 
@@ -1047,42 +1140,37 @@ describe("field resolvers", () => {
1047
1140
  const testRouter = router({ liveQuery });
1048
1141
  const app = createApp({ router: testRouter });
1049
1142
 
1050
- const results: unknown[] = [];
1143
+ const collector = createResultsCollector();
1051
1144
  const observable = app.execute({
1052
1145
  path: "liveQuery",
1053
1146
  input: { id: "1" },
1054
1147
  });
1055
1148
 
1056
1149
  const subscription = observable.subscribe({
1057
- next: (value) => results.push(value),
1150
+ next: (value) => collector.push(value as Message),
1058
1151
  });
1059
1152
 
1060
1153
  // Wait for initial result
1061
1154
  await new Promise((resolve) => setTimeout(resolve, 50));
1062
- expect(results.length).toBe(1);
1063
- const firstResult = results[0] as { data: { id: string; name: string } };
1064
- expect(firstResult.data.name).toBe("Initial");
1065
-
1066
- // Emit same value - should be skipped (equality check)
1067
- capturedEmit!({ id: "1", name: "Initial" });
1068
- await new Promise((resolve) => setTimeout(resolve, 50));
1069
- expect(results.length).toBe(1); // Still 1, not 2
1155
+ expect(collector.raw.length).toBe(1);
1156
+ const firstResult = collector.raw[0];
1157
+ expect(isSnapshot(firstResult)).toBe(true);
1158
+ if (isSnapshot(firstResult)) {
1159
+ expect(firstResult.data.name).toBe("Initial");
1160
+ }
1070
1161
 
1071
- // Emit same value again - should be skipped
1162
+ // STATELESS: All emits are forwarded (no deduplication)
1072
1163
  capturedEmit!({ id: "1", name: "Initial" });
1073
1164
  await new Promise((resolve) => setTimeout(resolve, 50));
1074
- expect(results.length).toBe(1); // Still 1
1165
+ expect(collector.raw.length).toBe(2); // Command forwarded
1075
1166
 
1076
- // Emit different value - should emit
1167
+ // Emit different value
1077
1168
  capturedEmit!({ id: "1", name: "Changed" });
1078
1169
  await new Promise((resolve) => setTimeout(resolve, 50));
1079
- expect(results.length).toBe(2); // Now 2
1080
- expect((results[1] as { data: { name: string } }).data.name).toBe("Changed");
1170
+ expect(collector.raw.length).toBe(3); // Another command
1081
1171
 
1082
- // Emit same "Changed" value again - should be skipped
1083
- capturedEmit!({ id: "1", name: "Changed" });
1084
- await new Promise((resolve) => setTimeout(resolve, 50));
1085
- expect(results.length).toBe(2); // Still 2
1172
+ // Client applies all updates to get final state
1173
+ expect((collector.current as { name: string }).name).toBe("Changed");
1086
1174
 
1087
1175
  subscription.unsubscribe();
1088
1176
  });
@@ -1154,7 +1242,7 @@ describe("field resolvers", () => {
1154
1242
  context: () => ({ db: mockDb }),
1155
1243
  });
1156
1244
 
1157
- const results: unknown[] = [];
1245
+ const collector = createResultsCollector();
1158
1246
  const subscription = server
1159
1247
  .execute({
1160
1248
  path: "getAuthor",
@@ -1169,7 +1257,7 @@ describe("field resolvers", () => {
1169
1257
  })
1170
1258
  .subscribe({
1171
1259
  next: (result) => {
1172
- results.push(result);
1260
+ collector.push(result as Message);
1173
1261
  },
1174
1262
  });
1175
1263
 
@@ -1177,13 +1265,16 @@ describe("field resolvers", () => {
1177
1265
  await new Promise((resolve) => setTimeout(resolve, 50));
1178
1266
 
1179
1267
  // Verify initial resolution
1180
- expect(results.length).toBe(1);
1268
+ expect(collector.raw.length).toBe(1);
1181
1269
  expect(resolveCallCount).toBe(1); // Resolver called once
1182
1270
  expect(subscribeCallCount).toBe(1); // Subscriber called once
1183
1271
  expect(capturedFieldEmit).toBeDefined(); // Emit captured from subscribe phase
1184
1272
 
1185
- const initialResult = results[0] as { data: { posts: { id: string }[] } };
1186
- expect(initialResult.data.posts).toHaveLength(2);
1273
+ const initialResult = collector.raw[0];
1274
+ expect(isSnapshot(initialResult)).toBe(true);
1275
+ if (isSnapshot(initialResult)) {
1276
+ expect((initialResult.data as { posts: { id: string }[] }).posts).toHaveLength(2);
1277
+ }
1187
1278
 
1188
1279
  // Use field emit to push update (from subscribe phase)
1189
1280
  const updatedPosts = [
@@ -1196,10 +1287,14 @@ describe("field resolvers", () => {
1196
1287
  // Wait for update
1197
1288
  await new Promise((resolve) => setTimeout(resolve, 50));
1198
1289
 
1199
- expect(results.length).toBe(2);
1200
- const updatedResult = results[1] as { data: { posts: { id: string; title: string }[] } };
1201
- expect(updatedResult.data.posts).toHaveLength(3);
1202
- expect(updatedResult.data.posts[2].title).toBe("New Post");
1290
+ // STATELESS: Second result is ops command
1291
+ expect(collector.raw.length).toBe(2);
1292
+ expect(isOps(collector.raw[1])).toBe(true);
1293
+
1294
+ // Client applies update to get final state
1295
+ const currentState = collector.current as { posts: { id: string; title: string }[] };
1296
+ expect(currentState.posts).toHaveLength(3);
1297
+ expect(currentState.posts[2].title).toBe("New Post");
1203
1298
 
1204
1299
  subscription.unsubscribe();
1205
1300
  });
@@ -1224,8 +1319,10 @@ describe("observable behavior", () => {
1224
1319
  }),
1225
1320
  );
1226
1321
 
1227
- expect(result.data).toEqual({ id: "1", name: "Test" });
1228
- expect(result.error).toBeUndefined();
1322
+ expect(isSnapshot(result)).toBe(true);
1323
+ if (isSnapshot(result)) {
1324
+ expect(result.data).toEqual({ id: "1", name: "Test" });
1325
+ }
1229
1326
  });
1230
1327
 
1231
1328
  it("keeps subscription open for potential emit", async () => {
@@ -1276,7 +1373,10 @@ describe("observable behavior", () => {
1276
1373
  }),
1277
1374
  );
1278
1375
 
1279
- expect(result.data).toEqual({ id: "new", name: "Test" });
1376
+ expect(isSnapshot(result)).toBe(true);
1377
+ if (isSnapshot(result)) {
1378
+ expect(result.data).toEqual({ id: "new", name: "Test" });
1379
+ }
1280
1380
  });
1281
1381
 
1282
1382
  it("can be unsubscribed", async () => {
@@ -1311,7 +1411,8 @@ describe("observable behavior", () => {
1311
1411
  // =============================================================================
1312
1412
 
1313
1413
  describe("emit backpressure", () => {
1314
- it("handles rapid emit calls without losing data", async () => {
1414
+ it("handles rapid emit calls (stateless - forwards all commands)", async () => {
1415
+ // STATELESS: Server forwards all emit commands synchronously
1315
1416
  type EmitFn = (data: unknown) => void;
1316
1417
  let capturedEmit: EmitFn | undefined;
1317
1418
 
@@ -1324,14 +1425,14 @@ describe("emit backpressure", () => {
1324
1425
 
1325
1426
  const server = createApp({ queries: { liveQuery } });
1326
1427
 
1327
- const results: unknown[] = [];
1428
+ const collector = createResultsCollector();
1328
1429
  const subscription = server
1329
1430
  .execute({
1330
1431
  path: "liveQuery",
1331
1432
  input: { id: "1" },
1332
1433
  })
1333
1434
  .subscribe({
1334
- next: (value) => results.push(value),
1435
+ next: (value) => collector.push(value as Message),
1335
1436
  });
1336
1437
 
1337
1438
  await new Promise((r) => setTimeout(r, 50));
@@ -1344,13 +1445,11 @@ describe("emit backpressure", () => {
1344
1445
  // Wait for all emits to process
1345
1446
  await new Promise((r) => setTimeout(r, 100));
1346
1447
 
1347
- // Should have received all unique values
1348
- // Note: deduplication may reduce count if emit is too fast
1349
- expect(results.length).toBeGreaterThan(1);
1448
+ // STATELESS: Should receive 1 initial + 10 updates (all forwarded)
1449
+ expect(collector.raw.length).toBe(11);
1350
1450
 
1351
- // The last result should have count = 10
1352
- const lastResult = results[results.length - 1] as { data: { count: number } };
1353
- expect(lastResult.data.count).toBe(10);
1451
+ // Final state after applying all updates
1452
+ expect((collector.current as { count: number }).count).toBe(10);
1354
1453
 
1355
1454
  subscription.unsubscribe();
1356
1455
  });
@@ -1377,8 +1476,10 @@ describe("observable error handling", () => {
1377
1476
  }),
1378
1477
  );
1379
1478
 
1380
- expect(result.error).toBeDefined();
1381
- expect(result.error?.message).toBe("Test error");
1479
+ expect(isError(result)).toBe(true);
1480
+ if (isError(result)) {
1481
+ expect(result.error).toBe("Test error");
1482
+ }
1382
1483
  });
1383
1484
 
1384
1485
  it("handles async resolver errors", async () => {
@@ -1398,8 +1499,10 @@ describe("observable error handling", () => {
1398
1499
  }),
1399
1500
  );
1400
1501
 
1401
- expect(result.error).toBeDefined();
1402
- expect(result.error?.message).toBe("Async error");
1502
+ expect(isError(result)).toBe(true);
1503
+ if (isError(result)) {
1504
+ expect(result.error).toBe("Async error");
1505
+ }
1403
1506
  });
1404
1507
  });
1405
1508
 
@@ -1747,21 +1850,25 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1747
1850
 
1748
1851
  const server = createApp({ queries: { liveUser } });
1749
1852
 
1750
- const results: unknown[] = [];
1853
+ const results: Message[] = [];
1751
1854
  const subscription = server
1752
1855
  .execute({
1753
1856
  path: "liveUser",
1754
1857
  input: { id: "1" },
1755
1858
  })
1756
1859
  .subscribe({
1757
- next: (result) => results.push(result),
1860
+ next: (result) => results.push(result as Message),
1758
1861
  });
1759
1862
 
1760
1863
  // Wait for initial result and subscriber setup
1761
1864
  await new Promise((r) => setTimeout(r, 50));
1762
1865
 
1763
1866
  expect(results.length).toBe(1);
1764
- expect((results[0] as { data: { name: string } }).data.name).toBe("Initial");
1867
+ const firstResult = results[0];
1868
+ expect(isSnapshot(firstResult)).toBe(true);
1869
+ if (isSnapshot(firstResult)) {
1870
+ expect(firstResult.data.name).toBe("Initial");
1871
+ }
1765
1872
  expect(subscriberCalled).toBe(true);
1766
1873
  expect(capturedEmit).toBeDefined();
1767
1874
  expect(capturedOnCleanup).toBeDefined();
@@ -1769,7 +1876,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1769
1876
  subscription.unsubscribe();
1770
1877
  });
1771
1878
 
1772
- it("emits updates from subscriber emit function", async () => {
1879
+ it("emits updates from subscriber emit function (stateless)", async () => {
1773
1880
  let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
1774
1881
 
1775
1882
  const liveUser = query()
@@ -1781,34 +1888,39 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1781
1888
 
1782
1889
  const server = createApp({ queries: { liveUser } });
1783
1890
 
1784
- const results: unknown[] = [];
1891
+ const collector = createResultsCollector();
1785
1892
  const subscription = server
1786
1893
  .execute({
1787
1894
  path: "liveUser",
1788
1895
  input: { id: "1" },
1789
1896
  })
1790
1897
  .subscribe({
1791
- next: (result) => results.push(result),
1898
+ next: (result) => collector.push(result as Message),
1792
1899
  });
1793
1900
 
1794
1901
  // Wait for initial result
1795
1902
  await new Promise((r) => setTimeout(r, 50));
1796
- expect(results.length).toBe(1);
1797
- expect((results[0] as { data: { name: string } }).data.name).toBe("Initial");
1903
+ expect(collector.raw.length).toBe(1);
1904
+ const firstResult = collector.raw[0];
1905
+ expect(isSnapshot(firstResult)).toBe(true);
1906
+ if (isSnapshot(firstResult)) {
1907
+ expect(firstResult.data.name).toBe("Initial");
1908
+ }
1798
1909
 
1799
- // Emit update via subscriber
1910
+ // Emit update via subscriber - STATELESS: sends ops command
1800
1911
  capturedEmit!({ id: "1", name: "Updated" });
1801
1912
  await new Promise((r) => setTimeout(r, 50));
1802
1913
 
1803
- expect(results.length).toBe(2);
1804
- expect((results[1] as { data: { name: string } }).data.name).toBe("Updated");
1914
+ expect(collector.raw.length).toBe(2);
1915
+ expect(isOps(collector.raw[1])).toBe(true); // Ops command
1916
+ expect((collector.current as { name: string }).name).toBe("Updated");
1805
1917
 
1806
1918
  // Emit another update
1807
1919
  capturedEmit!({ id: "1", name: "Updated Again" });
1808
1920
  await new Promise((r) => setTimeout(r, 50));
1809
1921
 
1810
- expect(results.length).toBe(3);
1811
- expect((results[2] as { data: { name: string } }).data.name).toBe("Updated Again");
1922
+ expect(collector.raw.length).toBe(3);
1923
+ expect((collector.current as { name: string }).name).toBe("Updated Again");
1812
1924
 
1813
1925
  subscription.unsubscribe();
1814
1926
  });
@@ -1892,8 +2004,8 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1892
2004
 
1893
2005
  const server = createApp({ queries: { liveUser } });
1894
2006
 
1895
- const results: unknown[] = [];
1896
- const errors: Error[] = [];
2007
+ const results: Message[] = [];
2008
+ const errors: string[] = [];
1897
2009
 
1898
2010
  const subscription = server
1899
2011
  .execute({
@@ -1902,10 +2014,11 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1902
2014
  })
1903
2015
  .subscribe({
1904
2016
  next: (result) => {
1905
- if (result.error) {
1906
- errors.push(result.error);
2017
+ const msg = result as Message;
2018
+ if (isError(msg)) {
2019
+ errors.push(msg.error);
1907
2020
  } else {
1908
- results.push(result);
2021
+ results.push(msg);
1909
2022
  }
1910
2023
  },
1911
2024
  });
@@ -1917,12 +2030,12 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1917
2030
  expect(results.length).toBe(1);
1918
2031
  // Error from subscriber should be reported
1919
2032
  expect(errors.length).toBe(1);
1920
- expect(errors[0].message).toBe("Subscriber error");
2033
+ expect(errors[0]).toBe("Subscriber error");
1921
2034
 
1922
2035
  subscription.unsubscribe();
1923
2036
  });
1924
2037
 
1925
- it("works with router-based operations", async () => {
2038
+ it("works with router-based operations (stateless)", async () => {
1926
2039
  let capturedEmit: ((value: { id: string; count: number }) => void) | undefined;
1927
2040
 
1928
2041
  const liveCounter = query()
@@ -1940,36 +2053,40 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1940
2053
 
1941
2054
  const server = createApp({ router: appRouter });
1942
2055
 
1943
- const results: unknown[] = [];
2056
+ const collector = createResultsCollector();
1944
2057
  const subscription = server
1945
2058
  .execute({
1946
2059
  path: "counter.live",
1947
2060
  input: { id: "c1" },
1948
2061
  })
1949
2062
  .subscribe({
1950
- next: (result) => results.push(result),
2063
+ next: (result) => collector.push(result as Message),
1951
2064
  });
1952
2065
 
1953
2066
  // Wait for initial result
1954
2067
  await new Promise((r) => setTimeout(r, 50));
1955
- expect(results.length).toBe(1);
1956
- expect((results[0] as { data: { count: number } }).data.count).toBe(0);
2068
+ expect(collector.raw.length).toBe(1);
2069
+ const firstResult = collector.raw[0];
2070
+ expect(isSnapshot(firstResult)).toBe(true);
2071
+ if (isSnapshot(firstResult)) {
2072
+ expect(firstResult.data.count).toBe(0);
2073
+ }
1957
2074
 
1958
- // Emit updates
2075
+ // Emit updates - STATELESS: sends ops commands
1959
2076
  capturedEmit!({ id: "c1", count: 1 });
1960
2077
  await new Promise((r) => setTimeout(r, 50));
1961
- expect(results.length).toBe(2);
1962
- expect((results[1] as { data: { count: number } }).data.count).toBe(1);
2078
+ expect(collector.raw.length).toBe(2);
2079
+ expect((collector.current as { count: number }).count).toBe(1);
1963
2080
 
1964
2081
  capturedEmit!({ id: "c1", count: 5 });
1965
2082
  await new Promise((r) => setTimeout(r, 50));
1966
- expect(results.length).toBe(3);
1967
- expect((results[2] as { data: { count: number } }).data.count).toBe(5);
2083
+ expect(collector.raw.length).toBe(3);
2084
+ expect((collector.current as { count: number }).count).toBe(5);
1968
2085
 
1969
2086
  subscription.unsubscribe();
1970
2087
  });
1971
2088
 
1972
- it("supports emit.merge for partial updates", async () => {
2089
+ it("supports emit.merge for partial updates (stateless)", async () => {
1973
2090
  type EmitFn = ((value: unknown) => void) & { merge: (partial: unknown) => void };
1974
2091
  let capturedEmit: EmitFn | undefined;
1975
2092
 
@@ -1982,36 +2099,42 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
1982
2099
 
1983
2100
  const server = createApp({ queries: { liveUser } });
1984
2101
 
1985
- const results: unknown[] = [];
2102
+ const collector = createResultsCollector();
1986
2103
  const subscription = server
1987
2104
  .execute({
1988
2105
  path: "liveUser",
1989
2106
  input: { id: "1" },
1990
2107
  })
1991
2108
  .subscribe({
1992
- next: (result) => results.push(result),
2109
+ next: (result) => collector.push(result as Message),
1993
2110
  });
1994
2111
 
1995
2112
  // Wait for initial result
1996
2113
  await new Promise((r) => setTimeout(r, 50));
1997
- expect(results.length).toBe(1);
1998
- const initial = (results[0] as { data: { name: string; status: string } }).data;
1999
- expect(initial.name).toBe("Initial");
2000
- expect(initial.status).toBe("offline");
2114
+ expect(collector.raw.length).toBe(1);
2115
+ const initialResult = collector.raw[0];
2116
+ expect(isSnapshot(initialResult)).toBe(true);
2117
+ if (isSnapshot(initialResult)) {
2118
+ expect(initialResult.data.name).toBe("Initial");
2119
+ expect(initialResult.data.status).toBe("offline");
2120
+ }
2001
2121
 
2002
- // Use merge for partial update
2122
+ // Use merge for partial update - STATELESS: sends ops command
2003
2123
  capturedEmit!.merge({ status: "online" });
2004
2124
  await new Promise((r) => setTimeout(r, 50));
2005
2125
 
2006
- expect(results.length).toBe(2);
2007
- const updated = (results[1] as { data: { name: string; status: string } }).data;
2126
+ expect(collector.raw.length).toBe(2);
2127
+ expect(isOps(collector.raw[1])).toBe(true); // Ops command
2128
+ const updated = collector.current as { name: string; status: string };
2008
2129
  expect(updated.name).toBe("Initial"); // Preserved
2009
2130
  expect(updated.status).toBe("online"); // Updated
2010
2131
 
2011
2132
  subscription.unsubscribe();
2012
2133
  });
2013
2134
 
2014
- it("deduplicates identical emit values", async () => {
2135
+ it("stateless server forwards all emits (deduplication is client responsibility)", async () => {
2136
+ // STATELESS: Server forwards all emit commands without deduplication.
2137
+ // Deduplication can be done by client or via optional plugins.
2015
2138
  let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
2016
2139
 
2017
2140
  const liveUser = query()
@@ -2023,34 +2146,37 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
2023
2146
 
2024
2147
  const server = createApp({ queries: { liveUser } });
2025
2148
 
2026
- const results: unknown[] = [];
2149
+ const collector = createResultsCollector();
2027
2150
  const subscription = server
2028
2151
  .execute({
2029
2152
  path: "liveUser",
2030
2153
  input: { id: "1" },
2031
2154
  })
2032
2155
  .subscribe({
2033
- next: (result) => results.push(result),
2156
+ next: (result) => collector.push(result as Message),
2034
2157
  });
2035
2158
 
2036
2159
  // Wait for initial result
2037
2160
  await new Promise((r) => setTimeout(r, 50));
2038
- expect(results.length).toBe(1);
2161
+ expect(collector.raw.length).toBe(1);
2039
2162
 
2040
- // Emit same value multiple times
2163
+ // STATELESS: All emits are forwarded (no server-side deduplication)
2041
2164
  capturedEmit!({ id: "1", name: "Initial" });
2042
2165
  await new Promise((r) => setTimeout(r, 50));
2043
- expect(results.length).toBe(1); // Should be deduplicated
2166
+ expect(collector.raw.length).toBe(2); // Forwarded, not deduplicated
2044
2167
 
2045
2168
  // Emit different value
2046
2169
  capturedEmit!({ id: "1", name: "Changed" });
2047
2170
  await new Promise((r) => setTimeout(r, 50));
2048
- expect(results.length).toBe(2);
2171
+ expect(collector.raw.length).toBe(3);
2049
2172
 
2050
- // Emit same changed value again
2173
+ // Emit same value again - still forwarded
2051
2174
  capturedEmit!({ id: "1", name: "Changed" });
2052
2175
  await new Promise((r) => setTimeout(r, 50));
2053
- expect(results.length).toBe(2); // Should be deduplicated
2176
+ expect(collector.raw.length).toBe(4); // All forwarded
2177
+
2178
+ // Final state is correct after applying all updates
2179
+ expect((collector.current as { name: string }).name).toBe("Changed");
2054
2180
 
2055
2181
  subscription.unsubscribe();
2056
2182
  });
@@ -2143,14 +2269,14 @@ describe("scalar field subscription with emit.delta()", () => {
2143
2269
  resolvers: [userResolver],
2144
2270
  });
2145
2271
 
2146
- const results: unknown[] = [];
2272
+ const results: Message[] = [];
2147
2273
  const subscription = server
2148
2274
  .execute({
2149
2275
  path: "getUserWithBio",
2150
2276
  input: { id: "1" },
2151
2277
  })
2152
2278
  .subscribe({
2153
- next: (result) => results.push(result),
2279
+ next: (result) => results.push(result as Message),
2154
2280
  });
2155
2281
 
2156
2282
  // Wait for subscription setup
@@ -2158,7 +2284,11 @@ describe("scalar field subscription with emit.delta()", () => {
2158
2284
 
2159
2285
  // Initial result
2160
2286
  expect(results.length).toBe(1);
2161
- expect((results[0] as { data: { bio: string } }).data.bio).toBe("Initial bio");
2287
+ const firstResult = results[0];
2288
+ expect(isSnapshot(firstResult)).toBe(true);
2289
+ if (isSnapshot(firstResult)) {
2290
+ expect((firstResult.data as { bio: string }).bio).toBe("Initial bio");
2291
+ }
2162
2292
 
2163
2293
  // Check that emit has delta method (EmitScalar)
2164
2294
  expect(capturedEmit).toBeDefined();
@@ -2167,7 +2297,7 @@ describe("scalar field subscription with emit.delta()", () => {
2167
2297
  subscription.unsubscribe();
2168
2298
  });
2169
2299
 
2170
- it("emit.delta() appends text to string field", async () => {
2300
+ it("emit.delta() sends delta command (stateless)", async () => {
2171
2301
  // UserWithContent - content field resolved by field resolver
2172
2302
  const UserWithContent = entity("UserWithContent", {
2173
2303
  id: t.id(),
@@ -2203,32 +2333,40 @@ describe("scalar field subscription with emit.delta()", () => {
2203
2333
  resolvers: [userResolver],
2204
2334
  });
2205
2335
 
2206
- const results: unknown[] = [];
2336
+ const collector = createResultsCollector();
2207
2337
  const subscription = server
2208
2338
  .execute({
2209
2339
  path: "getUserWithContent",
2210
2340
  input: { id: "1" },
2211
2341
  })
2212
2342
  .subscribe({
2213
- next: (result) => results.push(result),
2343
+ next: (result) => collector.push(result as Message),
2214
2344
  });
2215
2345
 
2216
2346
  // Wait for subscription setup
2217
2347
  await new Promise((r) => setTimeout(r, 50));
2218
2348
 
2219
2349
  // Initial result
2220
- expect(results.length).toBe(1);
2221
- expect((results[0] as { data: { content: string } }).data.content).toBe("Hello");
2350
+ expect(collector.raw.length).toBe(1);
2351
+ const initialResult = collector.raw[0];
2352
+ expect(isSnapshot(initialResult)).toBe(true);
2353
+ if (isSnapshot(initialResult)) {
2354
+ expect((initialResult.data as { content: string }).content).toBe("Hello");
2355
+ }
2222
2356
 
2223
- // Use emit.delta() to append text
2357
+ // Use emit.delta() to append text - STATELESS: sends ops command
2224
2358
  capturedEmit?.delta?.([{ position: Infinity, insert: " World" }]);
2225
2359
 
2226
2360
  // Wait for update
2227
2361
  await new Promise((r) => setTimeout(r, 50));
2228
2362
 
2229
- // Should have received update with delta applied
2230
- expect(results.length).toBe(2);
2231
- expect((results[1] as { data: { content: string } }).data.content).toBe("Hello World");
2363
+ // STATELESS: Received ops command
2364
+ expect(collector.raw.length).toBe(2);
2365
+ expect(isOps(collector.raw[1])).toBe(true);
2366
+
2367
+ // Client applies delta to get final state
2368
+ const currentState = collector.current as { content: string };
2369
+ expect(currentState.content).toBe("Hello World");
2232
2370
 
2233
2371
  subscription.unsubscribe();
2234
2372
  });