@sylphx/lens-server 1.5.1 → 1.5.6

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 CHANGED
@@ -238,21 +238,21 @@ interface LensServerConfig<
238
238
  TRouter extends RouterDef = RouterDef
239
239
  > {
240
240
  /** Entity definitions */
241
- entities?: EntitiesMap;
241
+ entities?: EntitiesMap | undefined;
242
242
  /** Router definition (namespaced operations) - context type is inferred */
243
- router?: TRouter;
243
+ router?: TRouter | undefined;
244
244
  /** Query definitions (flat, legacy) */
245
- queries?: QueriesMap;
245
+ queries?: QueriesMap | undefined;
246
246
  /** Mutation definitions (flat, legacy) */
247
- mutations?: MutationsMap;
247
+ mutations?: MutationsMap | undefined;
248
248
  /** Field resolvers array (use lens() factory to create) */
249
- resolvers?: Resolvers;
249
+ resolvers?: Resolvers | undefined;
250
250
  /** Logger for server messages (default: silent) */
251
- logger?: LensLogger;
251
+ logger?: LensLogger | undefined;
252
252
  /** Context factory - must return the context type expected by the router */
253
- context?: (req?: unknown) => TContext | Promise<TContext>;
253
+ context?: ((req?: unknown) => TContext | Promise<TContext>) | undefined;
254
254
  /** Server version */
255
- version?: string;
255
+ version?: string | undefined;
256
256
  }
257
257
  /** Server metadata for transport handshake */
258
258
  interface ServerMetadata {
package/dist/index.js CHANGED
@@ -11,7 +11,9 @@ import {
11
11
  createEmit,
12
12
  createUpdate as createUpdate2,
13
13
  flattenRouter,
14
+ isEntityDef,
14
15
  isMutationDef,
16
+ isPipeline,
15
17
  isQueryDef,
16
18
  runWithContext,
17
19
  toResolverMap
@@ -457,6 +459,70 @@ function createGraphStateManager(config) {
457
459
  }
458
460
 
459
461
  // src/server/create.ts
462
+ function getEntityTypeName(returnSpec) {
463
+ if (!returnSpec)
464
+ return;
465
+ if (isEntityDef(returnSpec)) {
466
+ return returnSpec._name;
467
+ }
468
+ if (Array.isArray(returnSpec) && returnSpec.length === 1 && isEntityDef(returnSpec[0])) {
469
+ return returnSpec[0]._name;
470
+ }
471
+ return;
472
+ }
473
+ function getInputFields(inputSchema) {
474
+ if (!inputSchema?.shape)
475
+ return [];
476
+ return Object.keys(inputSchema.shape);
477
+ }
478
+ function sugarToPipeline(optimistic, entityType, inputFields) {
479
+ if (isPipeline(optimistic)) {
480
+ return optimistic;
481
+ }
482
+ if (!entityType) {
483
+ return optimistic;
484
+ }
485
+ if (optimistic === "merge") {
486
+ const args = { type: entityType };
487
+ for (const field of inputFields) {
488
+ args[field] = { $input: field };
489
+ }
490
+ return {
491
+ $pipe: [{ $do: "entity.update", $with: args }]
492
+ };
493
+ }
494
+ if (optimistic === "create") {
495
+ const args = { type: entityType, id: { $temp: true } };
496
+ for (const field of inputFields) {
497
+ if (field !== "id") {
498
+ args[field] = { $input: field };
499
+ }
500
+ }
501
+ return {
502
+ $pipe: [{ $do: "entity.create", $with: args }]
503
+ };
504
+ }
505
+ if (optimistic === "delete") {
506
+ return {
507
+ $pipe: [{ $do: "entity.delete", $with: { type: entityType, id: { $input: "id" } } }]
508
+ };
509
+ }
510
+ if (typeof optimistic === "object" && optimistic !== null && "merge" in optimistic && typeof optimistic["merge"] === "object") {
511
+ const extra = optimistic["merge"];
512
+ const args = { type: entityType };
513
+ for (const field of inputFields) {
514
+ args[field] = { $input: field };
515
+ }
516
+ for (const [key, value] of Object.entries(extra)) {
517
+ args[key] = value;
518
+ }
519
+ return {
520
+ $pipe: [{ $do: "entity.update", $with: args }]
521
+ };
522
+ }
523
+ return optimistic;
524
+ }
525
+
460
526
  class DataLoader {
461
527
  batchFn;
462
528
  batch = new Map;
@@ -627,7 +693,9 @@ class LensServerImpl {
627
693
  for (const [name, def] of Object.entries(this.mutations)) {
628
694
  const meta = { type: "mutation" };
629
695
  if (def._optimistic) {
630
- meta.optimistic = def._optimistic;
696
+ const entityType = getEntityTypeName(def._output);
697
+ const inputFields = getInputFields(def._input);
698
+ meta.optimistic = sugarToPipeline(def._optimistic, entityType, inputFields);
631
699
  }
632
700
  setNested(name, meta);
633
701
  }
@@ -1141,7 +1209,7 @@ class LensServerImpl {
1141
1209
  const result = {};
1142
1210
  const obj = data;
1143
1211
  if ("id" in obj) {
1144
- result.id = obj.id;
1212
+ result["id"] = obj["id"];
1145
1213
  }
1146
1214
  if (Array.isArray(fields)) {
1147
1215
  for (const field of fields) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-server",
3
- "version": "1.5.1",
3
+ "version": "1.5.6",
4
4
  "description": "Server runtime for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "build": "bunup",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "test": "bun test",
18
- "prepack": "bun run build"
18
+ "prepack": "[ -d dist ] || bun run build"
19
19
  },
20
20
  "files": [
21
21
  "dist",
@@ -30,7 +30,7 @@
30
30
  "author": "SylphxAI",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@sylphx/lens-core": "^1.10.0"
33
+ "@sylphx/lens-core": "^1.15.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.9.3",
package/src/index.ts CHANGED
@@ -50,7 +50,7 @@ export {
50
50
  // Metadata types (for transport handshake)
51
51
  type ServerMetadata,
52
52
  type WebSocketLike,
53
- } from "./server/create";
53
+ } from "./server/create.js";
54
54
 
55
55
  // =============================================================================
56
56
  // State Management
@@ -68,7 +68,7 @@ export {
68
68
  type StateFullMessage,
69
69
  type StateUpdateMessage,
70
70
  type Subscription,
71
- } from "./state";
71
+ } from "./state/index.js";
72
72
 
73
73
  // =============================================================================
74
74
  // SSE Transport Adapter
@@ -82,4 +82,4 @@ export {
82
82
  SSEHandler,
83
83
  // Types
84
84
  type SSEHandlerConfig,
85
- } from "./sse/handler";
85
+ } from "./sse/handler.js";
@@ -950,8 +950,10 @@ describe("getMetadata", () => {
950
950
  expect(metadata.version).toBe("1.2.3");
951
951
  expect(metadata.operations).toBeDefined();
952
952
  expect(metadata.operations.getUser).toEqual({ type: "query" });
953
- // createUser auto-derives optimistic "create" from naming convention
954
- expect(metadata.operations.createUser).toEqual({ type: "mutation", optimistic: "create" });
953
+ // createUser auto-derives optimistic "create" from naming convention (converted to Pipeline)
954
+ expect((metadata.operations.createUser as any).type).toBe("mutation");
955
+ expect((metadata.operations.createUser as any).optimistic.$pipe).toBeDefined();
956
+ expect((metadata.operations.createUser as any).optimistic.$pipe[0].$do).toBe("entity.create");
955
957
  });
956
958
 
957
959
  it("builds nested operations map from namespaced routes", () => {
@@ -973,8 +975,10 @@ describe("getMetadata", () => {
973
975
  const metadata = server.getMetadata();
974
976
  expect(metadata.operations.user).toBeDefined();
975
977
  expect((metadata.operations.user as any).get).toEqual({ type: "query" });
976
- // Auto-derives optimistic "create" from naming convention
977
- expect((metadata.operations.user as any).create).toEqual({ type: "mutation", optimistic: "create" });
978
+ // Auto-derives optimistic "create" from naming convention (converted to Pipeline)
979
+ expect((metadata.operations.user as any).create.type).toBe("mutation");
980
+ expect((metadata.operations.user as any).create.optimistic.$pipe).toBeDefined();
981
+ expect((metadata.operations.user as any).create.optimistic.$pipe[0].$do).toBe("entity.create");
978
982
  });
979
983
 
980
984
  it("includes optimistic config in mutation metadata", () => {
@@ -990,9 +994,21 @@ describe("getMetadata", () => {
990
994
  });
991
995
 
992
996
  const metadata = server.getMetadata();
997
+ // Sugar "merge" is converted to Reify Pipeline
993
998
  expect(metadata.operations.updateUser).toEqual({
994
999
  type: "mutation",
995
- optimistic: "merge",
1000
+ optimistic: {
1001
+ $pipe: [
1002
+ {
1003
+ $do: "entity.update",
1004
+ $with: {
1005
+ type: "User",
1006
+ id: { $input: "id" },
1007
+ name: { $input: "name" },
1008
+ },
1009
+ },
1010
+ ],
1011
+ },
996
1012
  });
997
1013
  });
998
1014
 
@@ -18,12 +18,16 @@ import {
18
18
  type FieldType,
19
19
  flattenRouter,
20
20
  type InferRouterContext,
21
+ isEntityDef,
21
22
  isMutationDef,
23
+ isPipeline,
22
24
  isQueryDef,
23
25
  type MutationDef,
26
+ type Pipeline,
24
27
  type QueryDef,
25
28
  type ResolverDef,
26
29
  type Resolvers,
30
+ type ReturnSpec,
27
31
  type RouterDef,
28
32
  runWithContext,
29
33
  toResolverMap,
@@ -35,7 +39,7 @@ export interface SelectionObject {
35
39
  [key: string]: boolean | SelectionObject | { select: SelectionObject };
36
40
  }
37
41
 
38
- import { GraphStateManager } from "../state/graph-state-manager";
42
+ import { GraphStateManager } from "../state/graph-state-manager.js";
39
43
 
40
44
  // =============================================================================
41
45
  // Types
@@ -77,21 +81,21 @@ export interface LensServerConfig<
77
81
  TRouter extends RouterDef = RouterDef,
78
82
  > {
79
83
  /** Entity definitions */
80
- entities?: EntitiesMap;
84
+ entities?: EntitiesMap | undefined;
81
85
  /** Router definition (namespaced operations) - context type is inferred */
82
- router?: TRouter;
86
+ router?: TRouter | undefined;
83
87
  /** Query definitions (flat, legacy) */
84
- queries?: QueriesMap;
88
+ queries?: QueriesMap | undefined;
85
89
  /** Mutation definitions (flat, legacy) */
86
- mutations?: MutationsMap;
90
+ mutations?: MutationsMap | undefined;
87
91
  /** Field resolvers array (use lens() factory to create) */
88
- resolvers?: Resolvers;
92
+ resolvers?: Resolvers | undefined;
89
93
  /** Logger for server messages (default: silent) */
90
- logger?: LensLogger;
94
+ logger?: LensLogger | undefined;
91
95
  /** Context factory - must return the context type expected by the router */
92
- context?: (req?: unknown) => TContext | Promise<TContext>;
96
+ context?: ((req?: unknown) => TContext | Promise<TContext>) | undefined;
93
97
  /** Server version */
94
- version?: string;
98
+ version?: string | undefined;
95
99
  }
96
100
 
97
101
  /** Server metadata for transport handshake */
@@ -149,6 +153,121 @@ export interface WebSocketLike {
149
153
  onerror?: ((error: unknown) => void) | null;
150
154
  }
151
155
 
156
+ // =============================================================================
157
+ // Sugar to Reify Pipeline Conversion
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Extract entity type name from return spec.
162
+ * Returns undefined if not an entity.
163
+ */
164
+ function getEntityTypeName(returnSpec: ReturnSpec | undefined): string | undefined {
165
+ if (!returnSpec) return undefined;
166
+
167
+ // Single entity: EntityDef
168
+ if (isEntityDef(returnSpec)) {
169
+ return returnSpec._name;
170
+ }
171
+
172
+ // Array of entities: [EntityDef]
173
+ if (Array.isArray(returnSpec) && returnSpec.length === 1 && isEntityDef(returnSpec[0])) {
174
+ return returnSpec[0]._name;
175
+ }
176
+
177
+ return undefined;
178
+ }
179
+
180
+ /**
181
+ * Get input field keys from a Zod-like schema.
182
+ * Falls back to empty array if schema doesn't have shape.
183
+ */
184
+ function getInputFields(inputSchema: { shape?: Record<string, unknown> } | undefined): string[] {
185
+ if (!inputSchema?.shape) return [];
186
+ return Object.keys(inputSchema.shape);
187
+ }
188
+
189
+ /**
190
+ * Convert sugar syntax to Reify Pipeline.
191
+ *
192
+ * Sugar syntax:
193
+ * - "merge" → entity.update with input fields merged
194
+ * - "create" → entity.create with temp ID
195
+ * - "delete" → entity.delete by input.id
196
+ * - { merge: {...} } → entity.update with input + extra fields
197
+ *
198
+ * Returns the original value if already a Pipeline or not sugar.
199
+ */
200
+ function sugarToPipeline(
201
+ optimistic: unknown,
202
+ entityType: string | undefined,
203
+ inputFields: string[],
204
+ ): Pipeline | unknown {
205
+ // Already a Pipeline - pass through
206
+ if (isPipeline(optimistic)) {
207
+ return optimistic;
208
+ }
209
+
210
+ // No entity type - can't convert sugar
211
+ if (!entityType) {
212
+ return optimistic;
213
+ }
214
+
215
+ // "merge" sugar - update entity with input fields
216
+ if (optimistic === "merge") {
217
+ const args: Record<string, unknown> = { type: entityType };
218
+ for (const field of inputFields) {
219
+ args[field] = { $input: field };
220
+ }
221
+ return {
222
+ $pipe: [{ $do: "entity.update", $with: args }],
223
+ };
224
+ }
225
+
226
+ // "create" sugar - create entity with temp ID
227
+ if (optimistic === "create") {
228
+ const args: Record<string, unknown> = { type: entityType, id: { $temp: true } };
229
+ for (const field of inputFields) {
230
+ if (field !== "id") {
231
+ args[field] = { $input: field };
232
+ }
233
+ }
234
+ return {
235
+ $pipe: [{ $do: "entity.create", $with: args }],
236
+ };
237
+ }
238
+
239
+ // "delete" sugar - delete entity by input.id
240
+ if (optimistic === "delete") {
241
+ return {
242
+ $pipe: [{ $do: "entity.delete", $with: { type: entityType, id: { $input: "id" } } }],
243
+ };
244
+ }
245
+
246
+ // { merge: {...} } sugar - update with input + extra fields
247
+ if (
248
+ typeof optimistic === "object" &&
249
+ optimistic !== null &&
250
+ "merge" in optimistic &&
251
+ typeof (optimistic as Record<string, unknown>)["merge"] === "object"
252
+ ) {
253
+ const extra = (optimistic as { merge: Record<string, unknown> })["merge"];
254
+ const args: Record<string, unknown> = { type: entityType };
255
+ for (const field of inputFields) {
256
+ args[field] = { $input: field };
257
+ }
258
+ // Extra fields override input refs
259
+ for (const [key, value] of Object.entries(extra)) {
260
+ args[key] = value;
261
+ }
262
+ return {
263
+ $pipe: [{ $do: "entity.update", $with: args }],
264
+ };
265
+ }
266
+
267
+ // Unknown format - pass through
268
+ return optimistic;
269
+ }
270
+
152
271
  // =============================================================================
153
272
  // Protocol Messages
154
273
  // =============================================================================
@@ -309,7 +428,7 @@ class LensServerImpl<
309
428
  private queries: Q;
310
429
  private mutations: M;
311
430
  private entities: EntitiesMap;
312
- private resolverMap?: ResolverMap;
431
+ private resolverMap?: ResolverMap | undefined;
313
432
  private contextFactory: (req?: unknown) => TContext | Promise<TContext>;
314
433
  private version: string;
315
434
  private logger: LensLogger;
@@ -479,11 +598,14 @@ class LensServerImpl<
479
598
  setNested(name, { type: "query" });
480
599
  }
481
600
 
482
- // Add mutations with optimistic config
601
+ // Add mutations with optimistic config (convert sugar to Reify Pipeline)
483
602
  for (const [name, def] of Object.entries(this.mutations)) {
484
603
  const meta: OperationMeta = { type: "mutation" };
485
604
  if (def._optimistic) {
486
- meta.optimistic = def._optimistic;
605
+ // Convert sugar syntax to Reify Pipeline
606
+ const entityType = getEntityTypeName(def._output);
607
+ const inputFields = getInputFields(def._input as { shape?: Record<string, unknown> });
608
+ meta.optimistic = sugarToPipeline(def._optimistic, entityType, inputFields);
487
609
  }
488
610
  setNested(name, meta);
489
611
  }
@@ -1178,7 +1300,7 @@ class LensServerImpl<
1178
1300
 
1179
1301
  // Always include id
1180
1302
  if ("id" in obj) {
1181
- result.id = obj.id;
1303
+ result["id"] = obj["id"];
1182
1304
  }
1183
1305
 
1184
1306
  // Handle string array (simple field list)
@@ -5,7 +5,7 @@
5
5
  * Connects SSE streams to GraphStateManager.
6
6
  */
7
7
 
8
- import type { GraphStateManager, StateClient } from "../state/graph-state-manager";
8
+ import type { GraphStateManager, StateClient } from "../state/graph-state-manager.js";
9
9
 
10
10
  // =============================================================================
11
11
  // Types
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { beforeEach, describe, expect, it, mock } from "bun:test";
6
- import { GraphStateManager, type StateClient, type StateUpdateMessage } from "./graph-state-manager";
6
+ import { GraphStateManager, type StateClient, type StateUpdateMessage } from "./graph-state-manager.js";
7
7
 
8
8
  describe("GraphStateManager", () => {
9
9
  let manager: GraphStateManager;
@@ -62,7 +62,7 @@ describe("GraphStateManager", () => {
62
62
  entity: "Post",
63
63
  id: "123",
64
64
  });
65
- expect(mockClient.messages[0].updates.title).toMatchObject({
65
+ expect(mockClient.messages[0].updates["title"]).toMatchObject({
66
66
  strategy: "value",
67
67
  data: "Hello",
68
68
  });
@@ -179,7 +179,7 @@ describe("GraphStateManager", () => {
179
179
 
180
180
  manager.emit("Post", "123", { title: "World" });
181
181
 
182
- expect(mockClient.messages[0].updates.title.strategy).toBe("value");
182
+ expect(mockClient.messages[0].updates["title"].strategy).toBe("value");
183
183
  });
184
184
 
185
185
  it("uses delta strategy for long strings with small changes", () => {
@@ -191,7 +191,7 @@ describe("GraphStateManager", () => {
191
191
  manager.emit("Post", "123", { content: `${longText} appended` });
192
192
 
193
193
  // Should use delta for efficient transfer
194
- const update = mockClient.messages[0].updates.content;
194
+ const update = mockClient.messages[0].updates["content"];
195
195
  expect(["delta", "value"]).toContain(update.strategy);
196
196
  });
197
197
 
@@ -206,7 +206,7 @@ describe("GraphStateManager", () => {
206
206
  metadata: { views: 101, likes: 10, tags: ["a", "b"] },
207
207
  });
208
208
 
209
- const update = mockClient.messages[0].updates.metadata;
209
+ const update = mockClient.messages[0].updates["metadata"];
210
210
  expect(["patch", "value"]).toContain(update.strategy);
211
211
  });
212
212
  });
@@ -258,12 +258,12 @@ describe("GraphStateManager", () => {
258
258
 
259
259
  // client-1 got incremental update
260
260
  expect(mockClient.messages.length).toBe(1);
261
- expect(mockClient.messages[0].updates.title.data).toBe("Updated");
261
+ expect(mockClient.messages[0].updates["title"].data).toBe("Updated");
262
262
 
263
263
  // client-2 got full current state
264
264
  expect(client2.messages.length).toBe(1);
265
- expect(client2.messages[0].updates.title.data).toBe("Updated");
266
- expect(client2.messages[0].updates.content.data).toBe("World");
265
+ expect(client2.messages[0].updates["title"].data).toBe("Updated");
266
+ expect(client2.messages[0].updates["content"].data).toBe("World");
267
267
  });
268
268
  });
269
269
 
@@ -390,7 +390,7 @@ describe("GraphStateManager", () => {
390
390
  manager.emitField("Post", "123", "title", { strategy: "value", data: "Hello World" });
391
391
 
392
392
  expect(mockClient.messages.length).toBe(1);
393
- expect(mockClient.messages[0].updates.title).toEqual({
393
+ expect(mockClient.messages[0].updates["title"]).toEqual({
394
394
  strategy: "value",
395
395
  data: "Hello World",
396
396
  });
@@ -424,7 +424,7 @@ describe("GraphStateManager", () => {
424
424
  });
425
425
 
426
426
  const state = manager.getState("Post", "123");
427
- expect(state?.metadata).toEqual({ views: 101, likes: 10 });
427
+ expect(state?.["metadata"]).toEqual({ views: 101, likes: 10 });
428
428
  });
429
429
 
430
430
  it("sends field update to subscribed clients only for subscribed fields", () => {
@@ -480,9 +480,9 @@ describe("GraphStateManager", () => {
480
480
  ]);
481
481
 
482
482
  expect(mockClient.messages.length).toBe(1);
483
- expect(mockClient.messages[0].updates.title.data).toBe("Hello");
484
- expect(mockClient.messages[0].updates.content.data).toBe("World");
485
- expect(mockClient.messages[0].updates.author.data).toBe("Alice");
483
+ expect(mockClient.messages[0].updates["title"].data).toBe("Hello");
484
+ expect(mockClient.messages[0].updates["content"].data).toBe("World");
485
+ expect(mockClient.messages[0].updates["author"].data).toBe("Alice");
486
486
  });
487
487
 
488
488
  it("applies batch updates to canonical state", () => {
@@ -712,7 +712,7 @@ describe("GraphStateManager", () => {
712
712
  });
713
713
 
714
714
  expect(mockClient.messages.length).toBe(1);
715
- expect(mockClient.messages[0].updates.author.data).toEqual({
715
+ expect(mockClient.messages[0].updates["author"].data).toEqual({
716
716
  id: "1",
717
717
  name: "Alice",
718
718
  profile: {
@@ -792,7 +792,7 @@ describe("GraphStateManager", () => {
792
792
 
793
793
  // Should have 10 updates
794
794
  expect(mockClient.messages.length).toBe(10);
795
- expect(mockClient.messages[9].updates.counter.data).toBe(9);
795
+ expect(mockClient.messages[9].updates["counter"].data).toBe(9);
796
796
  });
797
797
 
798
798
  it("handles large number of subscribers to same entity", () => {
@@ -815,7 +815,7 @@ describe("GraphStateManager", () => {
815
815
  // All clients should receive the update
816
816
  for (const client of clients) {
817
817
  expect(client.messages.length).toBe(1);
818
- expect(client.messages[0].updates.title.data).toBe("Broadcast");
818
+ expect(client.messages[0].updates["title"].data).toBe("Broadcast");
819
819
  }
820
820
  });
821
821
 
@@ -842,14 +842,14 @@ describe("GraphStateManager", () => {
842
842
  manager.emit("Post", "123", { tags: ["javascript", "typescript"] });
843
843
 
844
844
  expect(mockClient.messages.length).toBe(1);
845
- expect(mockClient.messages[0].updates.tags.data).toEqual(["javascript", "typescript"]);
845
+ expect(mockClient.messages[0].updates["tags"].data).toEqual(["javascript", "typescript"]);
846
846
 
847
847
  // Update array
848
848
  mockClient.messages = [];
849
849
  manager.emit("Post", "123", { tags: ["javascript", "typescript", "react"] });
850
850
 
851
851
  expect(mockClient.messages.length).toBe(1);
852
- expect(mockClient.messages[0].updates.tags.data).toEqual(["javascript", "typescript", "react"]);
852
+ expect(mockClient.messages[0].updates["tags"].data).toEqual(["javascript", "typescript", "react"]);
853
853
  });
854
854
 
855
855
  it("handles boolean field values", () => {
@@ -859,14 +859,14 @@ describe("GraphStateManager", () => {
859
859
  manager.emit("Post", "123", { published: true });
860
860
 
861
861
  expect(mockClient.messages.length).toBe(1);
862
- expect(mockClient.messages[0].updates.published.data).toBe(true);
862
+ expect(mockClient.messages[0].updates["published"].data).toBe(true);
863
863
 
864
864
  // Toggle boolean
865
865
  mockClient.messages = [];
866
866
  manager.emit("Post", "123", { published: false });
867
867
 
868
868
  expect(mockClient.messages.length).toBe(1);
869
- expect(mockClient.messages[0].updates.published.data).toBe(false);
869
+ expect(mockClient.messages[0].updates["published"].data).toBe(false);
870
870
  });
871
871
 
872
872
  it("handles number field values including 0", () => {
@@ -876,14 +876,14 @@ describe("GraphStateManager", () => {
876
876
  manager.emit("Post", "123", { likes: 0 });
877
877
 
878
878
  expect(mockClient.messages.length).toBe(1);
879
- expect(mockClient.messages[0].updates.likes.data).toBe(0);
879
+ expect(mockClient.messages[0].updates["likes"].data).toBe(0);
880
880
 
881
881
  // Update to positive number
882
882
  mockClient.messages = [];
883
883
  manager.emit("Post", "123", { likes: 5 });
884
884
 
885
885
  expect(mockClient.messages.length).toBe(1);
886
- expect(mockClient.messages[0].updates.likes.data).toBe(5);
886
+ expect(mockClient.messages[0].updates["likes"].data).toBe(5);
887
887
  });
888
888
  });
889
889
 
@@ -909,7 +909,7 @@ describe("GraphStateManager", () => {
909
909
  entity: "Users",
910
910
  id: "list",
911
911
  });
912
- expect(mockClient.messages[0].updates._items.data).toEqual([
912
+ expect(mockClient.messages[0].updates["_items"].data).toEqual([
913
913
  { id: "1", name: "Alice" },
914
914
  { id: "2", name: "Bob" },
915
915
  ]);
@@ -1086,7 +1086,7 @@ describe("GraphStateManager", () => {
1086
1086
 
1087
1087
  expect(mockClient.messages.length).toBe(2);
1088
1088
  // Second message should be incremental diff (push operation)
1089
- const update = mockClient.messages[1].updates._items;
1089
+ const update = mockClient.messages[1].updates["_items"];
1090
1090
  expect(update.strategy).toBe("array");
1091
1091
  expect(update.data).toEqual([{ op: "push", item: { id: "2", name: "Bob" } }]);
1092
1092
  });
@@ -13,4 +13,4 @@ export {
13
13
  type StateFullMessage,
14
14
  type StateUpdateMessage,
15
15
  type Subscription,
16
- } from "./graph-state-manager";
16
+ } from "./graph-state-manager.js";