@sylphx/lens-server 2.10.0 → 2.11.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.js +23 -1
- package/package.json +2 -2
- package/src/server/create.test.ts +376 -0
- package/src/server/create.ts +29 -1
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
hashValue,
|
|
44
44
|
hasInlineResolvers,
|
|
45
45
|
isEntityDef,
|
|
46
|
+
isLiveQueryDef,
|
|
46
47
|
isModelDef,
|
|
47
48
|
isMutationDef,
|
|
48
49
|
isQueryDef,
|
|
@@ -622,7 +623,7 @@ class LensServerImpl {
|
|
|
622
623
|
currentState = state;
|
|
623
624
|
}, emitIfChanged, select, context, onCleanup) : undefined;
|
|
624
625
|
const lensContext = { ...context, emit, onCleanup };
|
|
625
|
-
const result = resolver({ input: cleanInput, ctx: lensContext });
|
|
626
|
+
const result = resolver({ args: cleanInput, input: cleanInput, ctx: lensContext });
|
|
626
627
|
if (isAsyncIterable(result)) {
|
|
627
628
|
for await (const value of result) {
|
|
628
629
|
if (cancelled)
|
|
@@ -639,6 +640,27 @@ class LensServerImpl {
|
|
|
639
640
|
currentState = value;
|
|
640
641
|
const processed = isQuery ? await this.processQueryResult(path, value, select, context, onCleanup, createFieldEmit) : value;
|
|
641
642
|
emitIfChanged(processed);
|
|
643
|
+
if (isQuery && isLiveQueryDef(def) && !cancelled) {
|
|
644
|
+
const liveQuery = def;
|
|
645
|
+
if (liveQuery._subscriber) {
|
|
646
|
+
try {
|
|
647
|
+
const publisher = liveQuery._subscriber({
|
|
648
|
+
args: cleanInput,
|
|
649
|
+
input: cleanInput,
|
|
650
|
+
ctx: context
|
|
651
|
+
});
|
|
652
|
+
if (publisher) {
|
|
653
|
+
publisher({ emit, onCleanup });
|
|
654
|
+
}
|
|
655
|
+
} catch (err) {
|
|
656
|
+
if (!cancelled) {
|
|
657
|
+
observer.next?.({
|
|
658
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
642
664
|
if (!isQuery && !cancelled) {
|
|
643
665
|
observer.complete?.();
|
|
644
666
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Server runtime for Lens API framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "SylphxAI",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@sylphx/lens-core": "^2.
|
|
33
|
+
"@sylphx/lens-core": "^2.9.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.9.3",
|
|
@@ -1725,3 +1725,379 @@ describe("Unified Entity Definition", () => {
|
|
|
1725
1725
|
});
|
|
1726
1726
|
});
|
|
1727
1727
|
});
|
|
1728
|
+
|
|
1729
|
+
// =============================================================================
|
|
1730
|
+
// Operation-Level .resolve().subscribe() Tests (LiveQueryDef)
|
|
1731
|
+
// =============================================================================
|
|
1732
|
+
|
|
1733
|
+
describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
1734
|
+
it("calls _subscriber after initial resolve for live updates", async () => {
|
|
1735
|
+
let subscriberCalled = false;
|
|
1736
|
+
let capturedEmit: ((value: unknown) => void) | undefined;
|
|
1737
|
+
let capturedOnCleanup: ((fn: () => void) => void) | undefined;
|
|
1738
|
+
|
|
1739
|
+
const liveUser = query()
|
|
1740
|
+
.input(z.object({ id: z.string() }))
|
|
1741
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
1742
|
+
.subscribe(({ input: _input }) => ({ emit, onCleanup }) => {
|
|
1743
|
+
subscriberCalled = true;
|
|
1744
|
+
capturedEmit = emit;
|
|
1745
|
+
capturedOnCleanup = onCleanup;
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
const server = createApp({ queries: { liveUser } });
|
|
1749
|
+
|
|
1750
|
+
const results: unknown[] = [];
|
|
1751
|
+
const subscription = server
|
|
1752
|
+
.execute({
|
|
1753
|
+
path: "liveUser",
|
|
1754
|
+
input: { id: "1" },
|
|
1755
|
+
})
|
|
1756
|
+
.subscribe({
|
|
1757
|
+
next: (result) => results.push(result),
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
// Wait for initial result and subscriber setup
|
|
1761
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1762
|
+
|
|
1763
|
+
expect(results.length).toBe(1);
|
|
1764
|
+
expect((results[0] as { data: { name: string } }).data.name).toBe("Initial");
|
|
1765
|
+
expect(subscriberCalled).toBe(true);
|
|
1766
|
+
expect(capturedEmit).toBeDefined();
|
|
1767
|
+
expect(capturedOnCleanup).toBeDefined();
|
|
1768
|
+
|
|
1769
|
+
subscription.unsubscribe();
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
it("emits updates from subscriber emit function", async () => {
|
|
1773
|
+
let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
|
|
1774
|
+
|
|
1775
|
+
const liveUser = query()
|
|
1776
|
+
.input(z.object({ id: z.string() }))
|
|
1777
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
1778
|
+
.subscribe(() => ({ emit }) => {
|
|
1779
|
+
capturedEmit = emit;
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
const server = createApp({ queries: { liveUser } });
|
|
1783
|
+
|
|
1784
|
+
const results: unknown[] = [];
|
|
1785
|
+
const subscription = server
|
|
1786
|
+
.execute({
|
|
1787
|
+
path: "liveUser",
|
|
1788
|
+
input: { id: "1" },
|
|
1789
|
+
})
|
|
1790
|
+
.subscribe({
|
|
1791
|
+
next: (result) => results.push(result),
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
// Wait for initial result
|
|
1795
|
+
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");
|
|
1798
|
+
|
|
1799
|
+
// Emit update via subscriber
|
|
1800
|
+
capturedEmit!({ id: "1", name: "Updated" });
|
|
1801
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1802
|
+
|
|
1803
|
+
expect(results.length).toBe(2);
|
|
1804
|
+
expect((results[1] as { data: { name: string } }).data.name).toBe("Updated");
|
|
1805
|
+
|
|
1806
|
+
// Emit another update
|
|
1807
|
+
capturedEmit!({ id: "1", name: "Updated Again" });
|
|
1808
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1809
|
+
|
|
1810
|
+
expect(results.length).toBe(3);
|
|
1811
|
+
expect((results[2] as { data: { name: string } }).data.name).toBe("Updated Again");
|
|
1812
|
+
|
|
1813
|
+
subscription.unsubscribe();
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
it("calls onCleanup when subscription is unsubscribed", async () => {
|
|
1817
|
+
let cleanupCalled = false;
|
|
1818
|
+
|
|
1819
|
+
const liveUser = query()
|
|
1820
|
+
.input(z.object({ id: z.string() }))
|
|
1821
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
1822
|
+
.subscribe(() => ({ onCleanup }) => {
|
|
1823
|
+
onCleanup(() => {
|
|
1824
|
+
cleanupCalled = true;
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
const server = createApp({ queries: { liveUser } });
|
|
1829
|
+
|
|
1830
|
+
const subscription = server
|
|
1831
|
+
.execute({
|
|
1832
|
+
path: "liveUser",
|
|
1833
|
+
input: { id: "1" },
|
|
1834
|
+
})
|
|
1835
|
+
.subscribe({});
|
|
1836
|
+
|
|
1837
|
+
// Wait for subscription setup
|
|
1838
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1839
|
+
expect(cleanupCalled).toBe(false);
|
|
1840
|
+
|
|
1841
|
+
// Unsubscribe
|
|
1842
|
+
subscription.unsubscribe();
|
|
1843
|
+
|
|
1844
|
+
// Cleanup should be called
|
|
1845
|
+
expect(cleanupCalled).toBe(true);
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it("passes input and context to subscriber", async () => {
|
|
1849
|
+
interface TestContext {
|
|
1850
|
+
userId: string;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
let receivedInput: { id: string } | undefined;
|
|
1854
|
+
let receivedCtx: TestContext | undefined;
|
|
1855
|
+
|
|
1856
|
+
const liveUser = query<TestContext>()
|
|
1857
|
+
.input(z.object({ id: z.string() }))
|
|
1858
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
1859
|
+
.subscribe(({ input, ctx }) => ({ emit: _emit }) => {
|
|
1860
|
+
receivedInput = input;
|
|
1861
|
+
receivedCtx = ctx;
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
const server = createApp({
|
|
1865
|
+
queries: { liveUser },
|
|
1866
|
+
context: () => ({ userId: "ctx-user-123" }),
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
const subscription = server
|
|
1870
|
+
.execute({
|
|
1871
|
+
path: "liveUser",
|
|
1872
|
+
input: { id: "input-123" },
|
|
1873
|
+
})
|
|
1874
|
+
.subscribe({});
|
|
1875
|
+
|
|
1876
|
+
// Wait for subscription setup
|
|
1877
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1878
|
+
|
|
1879
|
+
expect(receivedInput).toEqual({ id: "input-123" });
|
|
1880
|
+
expect(receivedCtx).toEqual({ userId: "ctx-user-123" });
|
|
1881
|
+
|
|
1882
|
+
subscription.unsubscribe();
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
it("handles subscriber errors gracefully", async () => {
|
|
1886
|
+
const liveUser = query()
|
|
1887
|
+
.input(z.object({ id: z.string() }))
|
|
1888
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
1889
|
+
.subscribe(() => () => {
|
|
1890
|
+
throw new Error("Subscriber error");
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
const server = createApp({ queries: { liveUser } });
|
|
1894
|
+
|
|
1895
|
+
const results: unknown[] = [];
|
|
1896
|
+
const errors: Error[] = [];
|
|
1897
|
+
|
|
1898
|
+
const subscription = server
|
|
1899
|
+
.execute({
|
|
1900
|
+
path: "liveUser",
|
|
1901
|
+
input: { id: "1" },
|
|
1902
|
+
})
|
|
1903
|
+
.subscribe({
|
|
1904
|
+
next: (result) => {
|
|
1905
|
+
if (result.error) {
|
|
1906
|
+
errors.push(result.error);
|
|
1907
|
+
} else {
|
|
1908
|
+
results.push(result);
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// Wait for execution
|
|
1914
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1915
|
+
|
|
1916
|
+
// Initial result should be delivered
|
|
1917
|
+
expect(results.length).toBe(1);
|
|
1918
|
+
// Error from subscriber should be reported
|
|
1919
|
+
expect(errors.length).toBe(1);
|
|
1920
|
+
expect(errors[0].message).toBe("Subscriber error");
|
|
1921
|
+
|
|
1922
|
+
subscription.unsubscribe();
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
it("works with router-based operations", async () => {
|
|
1926
|
+
let capturedEmit: ((value: { id: string; count: number }) => void) | undefined;
|
|
1927
|
+
|
|
1928
|
+
const liveCounter = query()
|
|
1929
|
+
.input(z.object({ id: z.string() }))
|
|
1930
|
+
.resolve(({ input }) => ({ id: input.id, count: 0 }))
|
|
1931
|
+
.subscribe(() => ({ emit }) => {
|
|
1932
|
+
capturedEmit = emit;
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
const appRouter = router({
|
|
1936
|
+
counter: router({
|
|
1937
|
+
live: liveCounter,
|
|
1938
|
+
}),
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
const server = createApp({ router: appRouter });
|
|
1942
|
+
|
|
1943
|
+
const results: unknown[] = [];
|
|
1944
|
+
const subscription = server
|
|
1945
|
+
.execute({
|
|
1946
|
+
path: "counter.live",
|
|
1947
|
+
input: { id: "c1" },
|
|
1948
|
+
})
|
|
1949
|
+
.subscribe({
|
|
1950
|
+
next: (result) => results.push(result),
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
// Wait for initial result
|
|
1954
|
+
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);
|
|
1957
|
+
|
|
1958
|
+
// Emit updates
|
|
1959
|
+
capturedEmit!({ id: "c1", count: 1 });
|
|
1960
|
+
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);
|
|
1963
|
+
|
|
1964
|
+
capturedEmit!({ id: "c1", count: 5 });
|
|
1965
|
+
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);
|
|
1968
|
+
|
|
1969
|
+
subscription.unsubscribe();
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
it("supports emit.merge for partial updates", async () => {
|
|
1973
|
+
type EmitFn = ((value: unknown) => void) & { merge: (partial: unknown) => void };
|
|
1974
|
+
let capturedEmit: EmitFn | undefined;
|
|
1975
|
+
|
|
1976
|
+
const liveUser = query()
|
|
1977
|
+
.input(z.object({ id: z.string() }))
|
|
1978
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial", status: "offline" }))
|
|
1979
|
+
.subscribe(() => ({ emit }) => {
|
|
1980
|
+
capturedEmit = emit as EmitFn;
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
const server = createApp({ queries: { liveUser } });
|
|
1984
|
+
|
|
1985
|
+
const results: unknown[] = [];
|
|
1986
|
+
const subscription = server
|
|
1987
|
+
.execute({
|
|
1988
|
+
path: "liveUser",
|
|
1989
|
+
input: { id: "1" },
|
|
1990
|
+
})
|
|
1991
|
+
.subscribe({
|
|
1992
|
+
next: (result) => results.push(result),
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
// Wait for initial result
|
|
1996
|
+
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");
|
|
2001
|
+
|
|
2002
|
+
// Use merge for partial update
|
|
2003
|
+
capturedEmit!.merge({ status: "online" });
|
|
2004
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2005
|
+
|
|
2006
|
+
expect(results.length).toBe(2);
|
|
2007
|
+
const updated = (results[1] as { data: { name: string; status: string } }).data;
|
|
2008
|
+
expect(updated.name).toBe("Initial"); // Preserved
|
|
2009
|
+
expect(updated.status).toBe("online"); // Updated
|
|
2010
|
+
|
|
2011
|
+
subscription.unsubscribe();
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
it("deduplicates identical emit values", async () => {
|
|
2015
|
+
let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
|
|
2016
|
+
|
|
2017
|
+
const liveUser = query()
|
|
2018
|
+
.input(z.object({ id: z.string() }))
|
|
2019
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
2020
|
+
.subscribe(() => ({ emit }) => {
|
|
2021
|
+
capturedEmit = emit;
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
const server = createApp({ queries: { liveUser } });
|
|
2025
|
+
|
|
2026
|
+
const results: unknown[] = [];
|
|
2027
|
+
const subscription = server
|
|
2028
|
+
.execute({
|
|
2029
|
+
path: "liveUser",
|
|
2030
|
+
input: { id: "1" },
|
|
2031
|
+
})
|
|
2032
|
+
.subscribe({
|
|
2033
|
+
next: (result) => results.push(result),
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
// Wait for initial result
|
|
2037
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2038
|
+
expect(results.length).toBe(1);
|
|
2039
|
+
|
|
2040
|
+
// Emit same value multiple times
|
|
2041
|
+
capturedEmit!({ id: "1", name: "Initial" });
|
|
2042
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2043
|
+
expect(results.length).toBe(1); // Should be deduplicated
|
|
2044
|
+
|
|
2045
|
+
// Emit different value
|
|
2046
|
+
capturedEmit!({ id: "1", name: "Changed" });
|
|
2047
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2048
|
+
expect(results.length).toBe(2);
|
|
2049
|
+
|
|
2050
|
+
// Emit same changed value again
|
|
2051
|
+
capturedEmit!({ id: "1", name: "Changed" });
|
|
2052
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2053
|
+
expect(results.length).toBe(2); // Should be deduplicated
|
|
2054
|
+
|
|
2055
|
+
subscription.unsubscribe();
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
it("multiple subscriptions each get their own subscriber instance", async () => {
|
|
2059
|
+
let subscriberCallCount = 0;
|
|
2060
|
+
const emits: Array<(value: { id: string; name: string }) => void> = [];
|
|
2061
|
+
|
|
2062
|
+
const liveUser = query()
|
|
2063
|
+
.input(z.object({ id: z.string() }))
|
|
2064
|
+
.resolve(({ input }) => ({ id: input.id, name: "Initial" }))
|
|
2065
|
+
.subscribe(() => ({ emit }) => {
|
|
2066
|
+
subscriberCallCount++;
|
|
2067
|
+
emits.push(emit);
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
const server = createApp({ queries: { liveUser } });
|
|
2071
|
+
|
|
2072
|
+
const results1: unknown[] = [];
|
|
2073
|
+
const results2: unknown[] = [];
|
|
2074
|
+
|
|
2075
|
+
const sub1 = server.execute({ path: "liveUser", input: { id: "1" } }).subscribe({ next: (r) => results1.push(r) });
|
|
2076
|
+
|
|
2077
|
+
const sub2 = server.execute({ path: "liveUser", input: { id: "2" } }).subscribe({ next: (r) => results2.push(r) });
|
|
2078
|
+
|
|
2079
|
+
// Wait for both subscriptions
|
|
2080
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2081
|
+
|
|
2082
|
+
expect(subscriberCallCount).toBe(2);
|
|
2083
|
+
expect(emits.length).toBe(2);
|
|
2084
|
+
expect(results1.length).toBe(1);
|
|
2085
|
+
expect(results2.length).toBe(1);
|
|
2086
|
+
|
|
2087
|
+
// Each emit only affects its subscription
|
|
2088
|
+
emits[0]({ id: "1", name: "Updated 1" });
|
|
2089
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2090
|
+
|
|
2091
|
+
expect(results1.length).toBe(2);
|
|
2092
|
+
expect(results2.length).toBe(1); // Unchanged
|
|
2093
|
+
|
|
2094
|
+
emits[1]({ id: "2", name: "Updated 2" });
|
|
2095
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2096
|
+
|
|
2097
|
+
expect(results1.length).toBe(2); // Unchanged
|
|
2098
|
+
expect(results2.length).toBe(2);
|
|
2099
|
+
|
|
2100
|
+
sub1.unsubscribe();
|
|
2101
|
+
sub2.unsubscribe();
|
|
2102
|
+
});
|
|
2103
|
+
});
|
package/src/server/create.ts
CHANGED
|
@@ -29,9 +29,11 @@ import {
|
|
|
29
29
|
hasInlineResolvers,
|
|
30
30
|
type InferRouterContext,
|
|
31
31
|
isEntityDef,
|
|
32
|
+
isLiveQueryDef,
|
|
32
33
|
isModelDef,
|
|
33
34
|
isMutationDef,
|
|
34
35
|
isQueryDef,
|
|
36
|
+
type LiveQueryDef,
|
|
35
37
|
mergeModelCollections,
|
|
36
38
|
type Observable,
|
|
37
39
|
type ResolverDef,
|
|
@@ -655,7 +657,7 @@ class LensServerImpl<
|
|
|
655
657
|
: undefined;
|
|
656
658
|
|
|
657
659
|
const lensContext = { ...context, emit, onCleanup };
|
|
658
|
-
const result = resolver({ input: cleanInput, ctx: lensContext });
|
|
660
|
+
const result = resolver({ args: cleanInput, input: cleanInput, ctx: lensContext });
|
|
659
661
|
|
|
660
662
|
if (isAsyncIterable(result)) {
|
|
661
663
|
// Streaming: emit each yielded value
|
|
@@ -691,6 +693,32 @@ class LensServerImpl<
|
|
|
691
693
|
: value;
|
|
692
694
|
emitIfChanged(processed);
|
|
693
695
|
|
|
696
|
+
// LiveQueryDef: Call _subscriber for live updates (Publisher pattern)
|
|
697
|
+
// ADR-002: .resolve().subscribe() pattern for operation-level live queries
|
|
698
|
+
if (isQuery && isLiveQueryDef(def) && !cancelled) {
|
|
699
|
+
const liveQuery = def as LiveQueryDef<unknown, unknown, TContext>;
|
|
700
|
+
if (liveQuery._subscriber) {
|
|
701
|
+
try {
|
|
702
|
+
// Get publisher function from subscriber
|
|
703
|
+
const publisher = liveQuery._subscriber({
|
|
704
|
+
args: cleanInput as never, // Preferred parameter name
|
|
705
|
+
input: cleanInput as never, // Deprecated alias for backwards compatibility
|
|
706
|
+
ctx: context as TContext,
|
|
707
|
+
});
|
|
708
|
+
// Call publisher with emit/onCleanup callbacks
|
|
709
|
+
if (publisher) {
|
|
710
|
+
publisher({ emit, onCleanup });
|
|
711
|
+
}
|
|
712
|
+
} catch (err) {
|
|
713
|
+
if (!cancelled) {
|
|
714
|
+
observer.next?.({
|
|
715
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
694
722
|
// Mutations complete immediately - they're truly one-shot
|
|
695
723
|
// Queries stay open for potential emit calls from field resolvers
|
|
696
724
|
if (!isQuery && !cancelled) {
|