@sylphx/lens-server 1.0.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +145 -11
- package/dist/index.js +267 -18
- package/package.json +2 -2
- package/src/e2e/server.test.ts +10 -10
- package/src/index.ts +21 -0
- package/src/server/create.test.ts +11 -11
- package/src/server/create.ts +125 -27
- package/src/state/graph-state-manager.test.ts +215 -0
- package/src/state/graph-state-manager.ts +423 -1
|
@@ -644,8 +644,8 @@ describe("Minimum Transfer", () => {
|
|
|
644
644
|
|
|
645
645
|
const liveQuery = query()
|
|
646
646
|
.returns(User)
|
|
647
|
-
.resolve(({
|
|
648
|
-
emitFn =
|
|
647
|
+
.resolve(({ emit }) => {
|
|
648
|
+
emitFn = emit;
|
|
649
649
|
return { id: "1", name: "Alice", email: "alice@example.com" };
|
|
650
650
|
});
|
|
651
651
|
|
|
@@ -681,13 +681,13 @@ describe("Minimum Transfer", () => {
|
|
|
681
681
|
}
|
|
682
682
|
});
|
|
683
683
|
|
|
684
|
-
it("sends updates via
|
|
684
|
+
it("sends updates via emit", async () => {
|
|
685
685
|
let emitFn: ((data: unknown) => void) | null = null;
|
|
686
686
|
|
|
687
687
|
const liveQuery = query()
|
|
688
688
|
.returns(User)
|
|
689
|
-
.resolve(({
|
|
690
|
-
emitFn =
|
|
689
|
+
.resolve(({ emit }) => {
|
|
690
|
+
emitFn = emit;
|
|
691
691
|
return { id: "1", name: "Alice", email: "alice@example.com" };
|
|
692
692
|
});
|
|
693
693
|
|
|
@@ -723,17 +723,17 @@ describe("Minimum Transfer", () => {
|
|
|
723
723
|
});
|
|
724
724
|
|
|
725
725
|
// =============================================================================
|
|
726
|
-
// Test:
|
|
726
|
+
// Test: onCleanup
|
|
727
727
|
// =============================================================================
|
|
728
728
|
|
|
729
|
-
describe("
|
|
729
|
+
describe("onCleanup", () => {
|
|
730
730
|
it("calls cleanup function on unsubscribe", async () => {
|
|
731
731
|
let cleanedUp = false;
|
|
732
732
|
|
|
733
733
|
const liveQuery = query()
|
|
734
734
|
.returns(User)
|
|
735
|
-
.resolve(({
|
|
736
|
-
|
|
735
|
+
.resolve(({ onCleanup }) => {
|
|
736
|
+
onCleanup(() => {
|
|
737
737
|
cleanedUp = true;
|
|
738
738
|
});
|
|
739
739
|
return mockUsers[0];
|
|
@@ -772,8 +772,8 @@ describe("ctx.onCleanup", () => {
|
|
|
772
772
|
|
|
773
773
|
const liveQuery = query()
|
|
774
774
|
.returns(User)
|
|
775
|
-
.resolve(({
|
|
776
|
-
|
|
775
|
+
.resolve(({ onCleanup }) => {
|
|
776
|
+
onCleanup(() => {
|
|
777
777
|
cleanedUp = true;
|
|
778
778
|
});
|
|
779
779
|
return mockUsers[0];
|
package/src/server/create.ts
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type ContextValue,
|
|
13
|
+
type Emit,
|
|
14
|
+
type EmitCommand,
|
|
13
15
|
type EntityDef,
|
|
14
16
|
type EntityDefinition,
|
|
15
17
|
type EntityResolvers,
|
|
16
18
|
type EntityResolversDefinition,
|
|
17
19
|
type FieldType,
|
|
20
|
+
type InferRouterContext,
|
|
18
21
|
type MutationDef,
|
|
19
22
|
type QueryDef,
|
|
20
23
|
type RelationDef,
|
|
@@ -22,6 +25,7 @@ import {
|
|
|
22
25
|
type RouterDef,
|
|
23
26
|
type Update,
|
|
24
27
|
createContext,
|
|
28
|
+
createEmit,
|
|
25
29
|
createUpdate,
|
|
26
30
|
flattenRouter,
|
|
27
31
|
isBatchResolver,
|
|
@@ -69,20 +73,23 @@ export type OperationsMap = {
|
|
|
69
73
|
};
|
|
70
74
|
|
|
71
75
|
/** Server configuration */
|
|
72
|
-
export interface LensServerConfig<
|
|
76
|
+
export interface LensServerConfig<
|
|
77
|
+
TContext extends ContextValue = ContextValue,
|
|
78
|
+
TRouter extends RouterDef = RouterDef,
|
|
79
|
+
> {
|
|
73
80
|
/** Entity definitions */
|
|
74
81
|
entities?: EntitiesMap;
|
|
75
82
|
/** Relation definitions */
|
|
76
83
|
relations?: RelationsArray;
|
|
77
|
-
/** Router definition (namespaced operations) */
|
|
78
|
-
router?:
|
|
84
|
+
/** Router definition (namespaced operations) - context type is inferred */
|
|
85
|
+
router?: TRouter;
|
|
79
86
|
/** Query definitions (flat, legacy) */
|
|
80
87
|
queries?: QueriesMap;
|
|
81
88
|
/** Mutation definitions (flat, legacy) */
|
|
82
89
|
mutations?: MutationsMap;
|
|
83
90
|
/** Entity resolvers */
|
|
84
91
|
resolvers?: EntityResolvers<EntityResolversDefinition>;
|
|
85
|
-
/** Context factory */
|
|
92
|
+
/** Context factory - must return the context type expected by the router */
|
|
86
93
|
context?: (req?: unknown) => TContext | Promise<TContext>;
|
|
87
94
|
/** Server version */
|
|
88
95
|
version?: string;
|
|
@@ -666,22 +673,37 @@ class LensServerImpl<
|
|
|
666
673
|
throw new Error(`Query ${sub.operation} has no resolver`);
|
|
667
674
|
}
|
|
668
675
|
|
|
669
|
-
//
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
}
|
|
676
|
+
// Create emit API for this subscription
|
|
677
|
+
const emit = createEmit((command: EmitCommand) => {
|
|
678
|
+
// Route emit commands to appropriate handler
|
|
679
|
+
const entityName = this.getEntityNameFromOutput(queryDef._output);
|
|
680
|
+
if (entityName) {
|
|
681
|
+
// For entity-typed outputs, use GraphStateManager
|
|
682
|
+
const entities = this.extractEntities(entityName, command.type === "full" ? command.data : {});
|
|
683
|
+
for (const { entity, id } of entities) {
|
|
684
|
+
this.stateManager.processCommand(entity, id, command);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Also emit the raw data for operation-level updates
|
|
688
|
+
if (command.type === "full") {
|
|
689
|
+
emitData(command.data);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Create onCleanup function
|
|
694
|
+
const onCleanup = (fn: () => void) => {
|
|
695
|
+
sub.cleanups.push(fn);
|
|
696
|
+
return () => {
|
|
697
|
+
const idx = sub.cleanups.indexOf(fn);
|
|
698
|
+
if (idx >= 0) sub.cleanups.splice(idx, 1);
|
|
699
|
+
};
|
|
680
700
|
};
|
|
681
701
|
|
|
682
702
|
const result = resolver({
|
|
683
703
|
input: sub.input,
|
|
684
|
-
ctx:
|
|
704
|
+
ctx: context,
|
|
705
|
+
emit,
|
|
706
|
+
onCleanup,
|
|
685
707
|
});
|
|
686
708
|
|
|
687
709
|
if (isAsyncIterable(result)) {
|
|
@@ -892,14 +914,16 @@ class LensServerImpl<
|
|
|
892
914
|
throw new Error(`Query ${name} has no resolver`);
|
|
893
915
|
}
|
|
894
916
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
emit: () => {},
|
|
899
|
-
onCleanup: () => () => {},
|
|
900
|
-
};
|
|
917
|
+
// Create no-op emit for one-shot queries (emit is only meaningful in subscriptions)
|
|
918
|
+
const emit = createEmit(() => {});
|
|
919
|
+
const onCleanup = () => () => {};
|
|
901
920
|
|
|
902
|
-
const result = resolver(
|
|
921
|
+
const result = resolver({
|
|
922
|
+
input: cleanInput as TInput,
|
|
923
|
+
ctx: context,
|
|
924
|
+
emit,
|
|
925
|
+
onCleanup,
|
|
926
|
+
});
|
|
903
927
|
|
|
904
928
|
let data: TOutput;
|
|
905
929
|
if (isAsyncIterable(result)) {
|
|
@@ -944,9 +968,15 @@ class LensServerImpl<
|
|
|
944
968
|
throw new Error(`Mutation ${name} has no resolver`);
|
|
945
969
|
}
|
|
946
970
|
|
|
971
|
+
// Create no-op emit for mutations (emit is primarily for subscriptions)
|
|
972
|
+
const emit = createEmit(() => {});
|
|
973
|
+
const onCleanup = () => () => {};
|
|
974
|
+
|
|
947
975
|
const result = await resolver({
|
|
948
976
|
input: input as TInput,
|
|
949
|
-
ctx: context,
|
|
977
|
+
ctx: context,
|
|
978
|
+
emit,
|
|
979
|
+
onCleanup,
|
|
950
980
|
});
|
|
951
981
|
|
|
952
982
|
// Emit to GraphStateManager
|
|
@@ -1520,17 +1550,85 @@ export type InferApi<T extends LensServer> = T extends LensServerImpl<infer Q, i
|
|
|
1520
1550
|
// Factory
|
|
1521
1551
|
// =============================================================================
|
|
1522
1552
|
|
|
1553
|
+
/**
|
|
1554
|
+
* Config helper type that infers context from router
|
|
1555
|
+
*/
|
|
1556
|
+
export type ServerConfigWithInferredContext<
|
|
1557
|
+
TRouter extends RouterDef,
|
|
1558
|
+
Q extends QueriesMap = QueriesMap,
|
|
1559
|
+
M extends MutationsMap = MutationsMap,
|
|
1560
|
+
> = {
|
|
1561
|
+
entities?: EntitiesMap;
|
|
1562
|
+
relations?: RelationsArray;
|
|
1563
|
+
router: TRouter;
|
|
1564
|
+
queries?: Q;
|
|
1565
|
+
mutations?: M;
|
|
1566
|
+
resolvers?: EntityResolvers<EntityResolversDefinition>;
|
|
1567
|
+
/** Context factory - type is inferred from router's procedures */
|
|
1568
|
+
context?: (req?: unknown) => InferRouterContext<TRouter> | Promise<InferRouterContext<TRouter>>;
|
|
1569
|
+
version?: string;
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Config without router (legacy flat queries/mutations)
|
|
1574
|
+
*/
|
|
1575
|
+
export type ServerConfigLegacy<
|
|
1576
|
+
TContext extends ContextValue = ContextValue,
|
|
1577
|
+
Q extends QueriesMap = QueriesMap,
|
|
1578
|
+
M extends MutationsMap = MutationsMap,
|
|
1579
|
+
> = {
|
|
1580
|
+
entities?: EntitiesMap;
|
|
1581
|
+
relations?: RelationsArray;
|
|
1582
|
+
router?: undefined;
|
|
1583
|
+
queries?: Q;
|
|
1584
|
+
mutations?: M;
|
|
1585
|
+
resolvers?: EntityResolvers<EntityResolversDefinition>;
|
|
1586
|
+
context?: (req?: unknown) => TContext | Promise<TContext>;
|
|
1587
|
+
version?: string;
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1523
1590
|
/**
|
|
1524
1591
|
* Create Lens server with Operations API + Optimization Layer
|
|
1592
|
+
*
|
|
1593
|
+
* When using a router with typed context (from initLens), the context
|
|
1594
|
+
* function's return type is automatically enforced to match.
|
|
1595
|
+
*
|
|
1596
|
+
* @example
|
|
1597
|
+
* ```typescript
|
|
1598
|
+
* // Context type is inferred from router's procedures
|
|
1599
|
+
* const server = createServer({
|
|
1600
|
+
* router: appRouter, // RouterDef with MyContext
|
|
1601
|
+
* context: () => ({
|
|
1602
|
+
* db: prisma,
|
|
1603
|
+
* user: null,
|
|
1604
|
+
* }), // Must match MyContext!
|
|
1605
|
+
* })
|
|
1606
|
+
* ```
|
|
1525
1607
|
*/
|
|
1608
|
+
export function createServer<
|
|
1609
|
+
TRouter extends RouterDef,
|
|
1610
|
+
Q extends QueriesMap = QueriesMap,
|
|
1611
|
+
M extends MutationsMap = MutationsMap,
|
|
1612
|
+
>(
|
|
1613
|
+
config: ServerConfigWithInferredContext<TRouter, Q, M>,
|
|
1614
|
+
): LensServer & { _types: { queries: Q; mutations: M; context: InferRouterContext<TRouter> } };
|
|
1615
|
+
|
|
1616
|
+
export function createServer<
|
|
1617
|
+
TContext extends ContextValue = ContextValue,
|
|
1618
|
+
Q extends QueriesMap = QueriesMap,
|
|
1619
|
+
M extends MutationsMap = MutationsMap,
|
|
1620
|
+
>(
|
|
1621
|
+
config: ServerConfigLegacy<TContext, Q, M>,
|
|
1622
|
+
): LensServer & { _types: { queries: Q; mutations: M; context: TContext } };
|
|
1623
|
+
|
|
1526
1624
|
export function createServer<
|
|
1527
1625
|
TContext extends ContextValue = ContextValue,
|
|
1528
1626
|
Q extends QueriesMap = QueriesMap,
|
|
1529
1627
|
M extends MutationsMap = MutationsMap,
|
|
1530
1628
|
>(
|
|
1531
1629
|
config: LensServerConfig<TContext> & { queries?: Q; mutations?: M },
|
|
1532
|
-
): LensServer & { _types: { queries: Q; mutations: M } } {
|
|
1533
|
-
const server = new LensServerImpl(config) as LensServerImpl<Q, M>;
|
|
1630
|
+
): LensServer & { _types: { queries: Q; mutations: M; context: TContext } } {
|
|
1631
|
+
const server = new LensServerImpl(config) as LensServerImpl<Q, M, TContext>;
|
|
1534
1632
|
// Attach type marker for inference (stripped at runtime)
|
|
1535
|
-
return server as unknown as LensServer & { _types: { queries: Q; mutations: M } };
|
|
1633
|
+
return server as unknown as LensServer & { _types: { queries: Q; mutations: M; context: TContext } };
|
|
1536
1634
|
}
|
|
@@ -331,4 +331,219 @@ describe("GraphStateManager", () => {
|
|
|
331
331
|
expect(stats.totalSubscriptions).toBe(3);
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
describe("array operations", () => {
|
|
336
|
+
interface User {
|
|
337
|
+
id: string;
|
|
338
|
+
name: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
it("emits array data", () => {
|
|
342
|
+
manager.subscribe("client-1", "Users", "list", "*");
|
|
343
|
+
mockClient.messages = [];
|
|
344
|
+
|
|
345
|
+
manager.emitArray("Users", "list", [
|
|
346
|
+
{ id: "1", name: "Alice" },
|
|
347
|
+
{ id: "2", name: "Bob" },
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
expect(mockClient.messages.length).toBe(1);
|
|
351
|
+
expect(mockClient.messages[0]).toMatchObject({
|
|
352
|
+
type: "update",
|
|
353
|
+
entity: "Users",
|
|
354
|
+
id: "list",
|
|
355
|
+
});
|
|
356
|
+
expect(mockClient.messages[0].updates._items.data).toEqual([
|
|
357
|
+
{ id: "1", name: "Alice" },
|
|
358
|
+
{ id: "2", name: "Bob" },
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("gets array state", () => {
|
|
363
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
364
|
+
|
|
365
|
+
expect(manager.getArrayState("Users", "list")).toEqual([{ id: "1", name: "Alice" }]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("applies push operation", () => {
|
|
369
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
370
|
+
manager.emitArrayOperation("Users", "list", {
|
|
371
|
+
op: "push",
|
|
372
|
+
item: { id: "2", name: "Bob" },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
376
|
+
{ id: "1", name: "Alice" },
|
|
377
|
+
{ id: "2", name: "Bob" },
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("applies unshift operation", () => {
|
|
382
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
383
|
+
manager.emitArrayOperation("Users", "list", {
|
|
384
|
+
op: "unshift",
|
|
385
|
+
item: { id: "0", name: "Zero" },
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
389
|
+
{ id: "0", name: "Zero" },
|
|
390
|
+
{ id: "1", name: "Alice" },
|
|
391
|
+
]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("applies insert operation", () => {
|
|
395
|
+
manager.emitArray("Users", "list", [
|
|
396
|
+
{ id: "1", name: "Alice" },
|
|
397
|
+
{ id: "3", name: "Charlie" },
|
|
398
|
+
]);
|
|
399
|
+
manager.emitArrayOperation("Users", "list", {
|
|
400
|
+
op: "insert",
|
|
401
|
+
index: 1,
|
|
402
|
+
item: { id: "2", name: "Bob" },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
406
|
+
{ id: "1", name: "Alice" },
|
|
407
|
+
{ id: "2", name: "Bob" },
|
|
408
|
+
{ id: "3", name: "Charlie" },
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("applies remove operation", () => {
|
|
413
|
+
manager.emitArray("Users", "list", [
|
|
414
|
+
{ id: "1", name: "Alice" },
|
|
415
|
+
{ id: "2", name: "Bob" },
|
|
416
|
+
]);
|
|
417
|
+
manager.emitArrayOperation("Users", "list", { op: "remove", index: 0 });
|
|
418
|
+
|
|
419
|
+
expect(manager.getArrayState("Users", "list")).toEqual([{ id: "2", name: "Bob" }]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("applies removeById operation", () => {
|
|
423
|
+
manager.emitArray("Users", "list", [
|
|
424
|
+
{ id: "1", name: "Alice" },
|
|
425
|
+
{ id: "2", name: "Bob" },
|
|
426
|
+
]);
|
|
427
|
+
manager.emitArrayOperation("Users", "list", { op: "removeById", id: "1" });
|
|
428
|
+
|
|
429
|
+
expect(manager.getArrayState("Users", "list")).toEqual([{ id: "2", name: "Bob" }]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("handles removeById for non-existent id", () => {
|
|
433
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
434
|
+
manager.emitArrayOperation("Users", "list", { op: "removeById", id: "999" });
|
|
435
|
+
|
|
436
|
+
expect(manager.getArrayState("Users", "list")).toEqual([{ id: "1", name: "Alice" }]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("applies update operation", () => {
|
|
440
|
+
manager.emitArray("Users", "list", [
|
|
441
|
+
{ id: "1", name: "Alice" },
|
|
442
|
+
{ id: "2", name: "Bob" },
|
|
443
|
+
]);
|
|
444
|
+
manager.emitArrayOperation("Users", "list", {
|
|
445
|
+
op: "update",
|
|
446
|
+
index: 1,
|
|
447
|
+
item: { id: "2", name: "Robert" },
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
451
|
+
{ id: "1", name: "Alice" },
|
|
452
|
+
{ id: "2", name: "Robert" },
|
|
453
|
+
]);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("applies updateById operation", () => {
|
|
457
|
+
manager.emitArray("Users", "list", [
|
|
458
|
+
{ id: "1", name: "Alice" },
|
|
459
|
+
{ id: "2", name: "Bob" },
|
|
460
|
+
]);
|
|
461
|
+
manager.emitArrayOperation("Users", "list", {
|
|
462
|
+
op: "updateById",
|
|
463
|
+
id: "2",
|
|
464
|
+
item: { id: "2", name: "Robert" },
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
468
|
+
{ id: "1", name: "Alice" },
|
|
469
|
+
{ id: "2", name: "Robert" },
|
|
470
|
+
]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("applies merge operation", () => {
|
|
474
|
+
manager.emitArray("Users", "list", [
|
|
475
|
+
{ id: "1", name: "Alice" },
|
|
476
|
+
{ id: "2", name: "Bob" },
|
|
477
|
+
]);
|
|
478
|
+
manager.emitArrayOperation("Users", "list", {
|
|
479
|
+
op: "merge",
|
|
480
|
+
index: 0,
|
|
481
|
+
partial: { name: "Alicia" },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
485
|
+
{ id: "1", name: "Alicia" },
|
|
486
|
+
{ id: "2", name: "Bob" },
|
|
487
|
+
]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("applies mergeById operation", () => {
|
|
491
|
+
manager.emitArray("Users", "list", [
|
|
492
|
+
{ id: "1", name: "Alice" },
|
|
493
|
+
{ id: "2", name: "Bob" },
|
|
494
|
+
]);
|
|
495
|
+
manager.emitArrayOperation("Users", "list", {
|
|
496
|
+
op: "mergeById",
|
|
497
|
+
id: "2",
|
|
498
|
+
partial: { name: "Bobby" },
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
502
|
+
{ id: "1", name: "Alice" },
|
|
503
|
+
{ id: "2", name: "Bobby" },
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("processCommand handles array operations", () => {
|
|
508
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
509
|
+
|
|
510
|
+
manager.processCommand("Users", "list", {
|
|
511
|
+
type: "array",
|
|
512
|
+
operation: { op: "push", item: { id: "2", name: "Bob" } },
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(manager.getArrayState("Users", "list")).toEqual([
|
|
516
|
+
{ id: "1", name: "Alice" },
|
|
517
|
+
{ id: "2", name: "Bob" },
|
|
518
|
+
]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("sends array updates to subscribed clients", () => {
|
|
522
|
+
manager.subscribe("client-1", "Users", "list", "*");
|
|
523
|
+
mockClient.messages = [];
|
|
524
|
+
|
|
525
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
526
|
+
manager.emitArrayOperation("Users", "list", {
|
|
527
|
+
op: "push",
|
|
528
|
+
item: { id: "2", name: "Bob" },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(mockClient.messages.length).toBe(2);
|
|
532
|
+
expect(mockClient.messages[1].updates._items.data).toEqual([
|
|
533
|
+
{ id: "1", name: "Alice" },
|
|
534
|
+
{ id: "2", name: "Bob" },
|
|
535
|
+
]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("does not send update if array unchanged", () => {
|
|
539
|
+
manager.subscribe("client-1", "Users", "list", "*");
|
|
540
|
+
manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
|
|
541
|
+
mockClient.messages = [];
|
|
542
|
+
|
|
543
|
+
// Remove by non-existent id (no change)
|
|
544
|
+
manager.emitArrayOperation("Users", "list", { op: "removeById", id: "999" });
|
|
545
|
+
|
|
546
|
+
expect(mockClient.messages.length).toBe(0);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
334
549
|
});
|