@sylphx/lens-solid 2.3.4 → 2.3.5

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.
Files changed (2) hide show
  1. package/dist/index.js +737 -303
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1069,6 +1069,217 @@ class ReconnectionMetricsTracker {
1069
1069
  return sorted[Math.max(0, index)];
1070
1070
  }
1071
1071
  }
1072
+ class SubscriptionRegistry {
1073
+ subscriptions = new Map;
1074
+ entityIndex = new Map;
1075
+ add(sub) {
1076
+ const tracked = {
1077
+ ...sub,
1078
+ state: "pending",
1079
+ lastDataHash: sub.lastData ? hashEntityState(sub.lastData) : null,
1080
+ createdAt: Date.now(),
1081
+ lastUpdateAt: null
1082
+ };
1083
+ this.subscriptions.set(sub.id, tracked);
1084
+ const entityKey = `${sub.entity}:${sub.entityId}`;
1085
+ let ids = this.entityIndex.get(entityKey);
1086
+ if (!ids) {
1087
+ ids = new Set;
1088
+ this.entityIndex.set(entityKey, ids);
1089
+ }
1090
+ ids.add(sub.id);
1091
+ }
1092
+ get(id2) {
1093
+ return this.subscriptions.get(id2);
1094
+ }
1095
+ has(id2) {
1096
+ return this.subscriptions.has(id2);
1097
+ }
1098
+ remove(id2) {
1099
+ const sub = this.subscriptions.get(id2);
1100
+ if (!sub)
1101
+ return;
1102
+ this.subscriptions.delete(id2);
1103
+ const entityKey = `${sub.entity}:${sub.entityId}`;
1104
+ const ids = this.entityIndex.get(entityKey);
1105
+ if (ids) {
1106
+ ids.delete(id2);
1107
+ if (ids.size === 0) {
1108
+ this.entityIndex.delete(entityKey);
1109
+ }
1110
+ }
1111
+ }
1112
+ getByEntity(entity22, entityId) {
1113
+ const entityKey = `${entity22}:${entityId}`;
1114
+ const ids = this.entityIndex.get(entityKey);
1115
+ if (!ids)
1116
+ return [];
1117
+ const result = [];
1118
+ for (const id2 of ids) {
1119
+ const sub = this.subscriptions.get(id2);
1120
+ if (sub) {
1121
+ result.push(sub);
1122
+ }
1123
+ }
1124
+ return result;
1125
+ }
1126
+ updateVersion(id2, version, data) {
1127
+ const sub = this.subscriptions.get(id2);
1128
+ if (!sub)
1129
+ return;
1130
+ sub.version = version;
1131
+ sub.lastUpdateAt = Date.now();
1132
+ if (data !== undefined) {
1133
+ sub.lastData = data;
1134
+ sub.lastDataHash = hashEntityState(data);
1135
+ }
1136
+ if (sub.state === "pending" || sub.state === "reconnecting") {
1137
+ sub.state = "active";
1138
+ }
1139
+ }
1140
+ updateData(id2, data) {
1141
+ const sub = this.subscriptions.get(id2);
1142
+ if (!sub)
1143
+ return;
1144
+ sub.lastData = data;
1145
+ sub.lastDataHash = hashEntityState(data);
1146
+ }
1147
+ getLastData(id2) {
1148
+ return this.subscriptions.get(id2)?.lastData ?? null;
1149
+ }
1150
+ getVersion(id2) {
1151
+ return this.subscriptions.get(id2)?.version ?? null;
1152
+ }
1153
+ markActive(id2) {
1154
+ const sub = this.subscriptions.get(id2);
1155
+ if (sub) {
1156
+ sub.state = "active";
1157
+ }
1158
+ }
1159
+ markError(id2) {
1160
+ const sub = this.subscriptions.get(id2);
1161
+ if (sub) {
1162
+ sub.state = "error";
1163
+ }
1164
+ }
1165
+ markAllReconnecting() {
1166
+ for (const sub of this.subscriptions.values()) {
1167
+ if (sub.state === "active") {
1168
+ sub.state = "reconnecting";
1169
+ }
1170
+ }
1171
+ }
1172
+ getByState(state) {
1173
+ const result = [];
1174
+ for (const sub of this.subscriptions.values()) {
1175
+ if (sub.state === state) {
1176
+ result.push(sub);
1177
+ }
1178
+ }
1179
+ return result;
1180
+ }
1181
+ getAllForReconnect() {
1182
+ const result = [];
1183
+ for (const sub of this.subscriptions.values()) {
1184
+ if (sub.state === "reconnecting" || sub.state === "active") {
1185
+ const reconnectSub = {
1186
+ id: sub.id,
1187
+ entity: sub.entity,
1188
+ entityId: sub.entityId,
1189
+ fields: sub.fields,
1190
+ version: sub.version,
1191
+ input: sub.input
1192
+ };
1193
+ if (sub.lastDataHash) {
1194
+ reconnectSub.dataHash = sub.lastDataHash;
1195
+ }
1196
+ result.push(reconnectSub);
1197
+ }
1198
+ }
1199
+ return result;
1200
+ }
1201
+ processReconnectResult(id2, version, data) {
1202
+ const sub = this.subscriptions.get(id2);
1203
+ if (!sub)
1204
+ return;
1205
+ sub.version = version;
1206
+ sub.state = "active";
1207
+ sub.lastUpdateAt = Date.now();
1208
+ if (data !== undefined) {
1209
+ sub.lastData = data;
1210
+ sub.lastDataHash = hashEntityState(data);
1211
+ }
1212
+ }
1213
+ getObserver(id2) {
1214
+ return this.subscriptions.get(id2)?.observer;
1215
+ }
1216
+ updateObserver(id2, observer) {
1217
+ const sub = this.subscriptions.get(id2);
1218
+ if (sub) {
1219
+ sub.observer = observer;
1220
+ }
1221
+ }
1222
+ notifyNext(id2, data) {
1223
+ const sub = this.subscriptions.get(id2);
1224
+ sub?.observer.next?.({ data, version: sub.version });
1225
+ }
1226
+ notifyError(id2, error) {
1227
+ this.subscriptions.get(id2)?.observer.error?.(error);
1228
+ }
1229
+ notifyAllReconnectingError(error) {
1230
+ for (const sub of this.subscriptions.values()) {
1231
+ if (sub.state === "reconnecting") {
1232
+ sub.observer.error?.(error);
1233
+ }
1234
+ }
1235
+ }
1236
+ get size() {
1237
+ return this.subscriptions.size;
1238
+ }
1239
+ getIds() {
1240
+ return Array.from(this.subscriptions.keys());
1241
+ }
1242
+ values() {
1243
+ return this.subscriptions.values();
1244
+ }
1245
+ getStats() {
1246
+ const byState = {
1247
+ pending: 0,
1248
+ active: 0,
1249
+ reconnecting: 0,
1250
+ error: 0
1251
+ };
1252
+ const byEntity = {};
1253
+ for (const sub of this.subscriptions.values()) {
1254
+ byState[sub.state]++;
1255
+ const entityKey = `${sub.entity}:${sub.entityId}`;
1256
+ byEntity[entityKey] = (byEntity[entityKey] ?? 0) + 1;
1257
+ }
1258
+ return {
1259
+ total: this.subscriptions.size,
1260
+ byState,
1261
+ byEntity
1262
+ };
1263
+ }
1264
+ clear() {
1265
+ for (const sub of this.subscriptions.values()) {
1266
+ sub.observer.complete?.();
1267
+ }
1268
+ this.subscriptions.clear();
1269
+ this.entityIndex.clear();
1270
+ }
1271
+ clearErrors() {
1272
+ const toRemove = [];
1273
+ for (const [id2, sub] of this.subscriptions) {
1274
+ if (sub.state === "error") {
1275
+ toRemove.push(id2);
1276
+ }
1277
+ }
1278
+ for (const id2 of toRemove) {
1279
+ this.remove(id2);
1280
+ }
1281
+ }
1282
+ }
1072
1283
  function applyOps(state, ops) {
1073
1284
  let result = state;
1074
1285
  for (const op2 of ops) {
@@ -1310,6 +1521,282 @@ function isError(msg) {
1310
1521
  }
1311
1522
 
1312
1523
  // ../client/dist/index.js
1524
+ class SelectionRegistry {
1525
+ endpoints = new Map;
1526
+ addSubscriber(params) {
1527
+ const { endpointKey, subscriberId, selection, onData, onError } = params;
1528
+ let endpoint = this.endpoints.get(endpointKey);
1529
+ const previousSelection = endpoint ? { ...endpoint.mergedSelection } : {};
1530
+ if (!endpoint) {
1531
+ endpoint = {
1532
+ key: endpointKey,
1533
+ subscribers: new Map,
1534
+ mergedSelection: {},
1535
+ lastData: null,
1536
+ isSubscribed: false,
1537
+ createdAt: Date.now(),
1538
+ lastSelectionChangeAt: null
1539
+ };
1540
+ this.endpoints.set(endpointKey, endpoint);
1541
+ }
1542
+ const subscriberMeta = {
1543
+ id: subscriberId,
1544
+ selection,
1545
+ onData,
1546
+ createdAt: Date.now()
1547
+ };
1548
+ if (onError) {
1549
+ subscriberMeta.onError = onError;
1550
+ }
1551
+ endpoint.subscribers.set(subscriberId, subscriberMeta);
1552
+ const newSelection = this.computeMergedSelection(endpoint);
1553
+ const analysis = this.analyzeSelectionChange(previousSelection, newSelection);
1554
+ if (analysis.hasChanged) {
1555
+ endpoint.mergedSelection = newSelection;
1556
+ endpoint.lastSelectionChangeAt = Date.now();
1557
+ }
1558
+ return analysis;
1559
+ }
1560
+ removeSubscriber(endpointKey, subscriberId) {
1561
+ const endpoint = this.endpoints.get(endpointKey);
1562
+ if (!endpoint) {
1563
+ return this.noChangeAnalysis();
1564
+ }
1565
+ const previousSelection = { ...endpoint.mergedSelection };
1566
+ endpoint.subscribers.delete(subscriberId);
1567
+ if (endpoint.subscribers.size === 0) {
1568
+ this.endpoints.delete(endpointKey);
1569
+ return this.analyzeSelectionChange(previousSelection, {});
1570
+ }
1571
+ const newSelection = this.computeMergedSelection(endpoint);
1572
+ const analysis = this.analyzeSelectionChange(previousSelection, newSelection);
1573
+ if (analysis.hasChanged) {
1574
+ endpoint.mergedSelection = newSelection;
1575
+ endpoint.lastSelectionChangeAt = Date.now();
1576
+ }
1577
+ return analysis;
1578
+ }
1579
+ getMergedSelection(endpointKey) {
1580
+ return this.endpoints.get(endpointKey)?.mergedSelection ?? null;
1581
+ }
1582
+ getSubscriberIds(endpointKey) {
1583
+ const endpoint = this.endpoints.get(endpointKey);
1584
+ return endpoint ? Array.from(endpoint.subscribers.keys()) : [];
1585
+ }
1586
+ getSubscriberCount(endpointKey) {
1587
+ return this.endpoints.get(endpointKey)?.subscribers.size ?? 0;
1588
+ }
1589
+ hasSubscribers(endpointKey) {
1590
+ return (this.endpoints.get(endpointKey)?.subscribers.size ?? 0) > 0;
1591
+ }
1592
+ distributeData(endpointKey, data) {
1593
+ const endpoint = this.endpoints.get(endpointKey);
1594
+ if (!endpoint)
1595
+ return;
1596
+ endpoint.lastData = data;
1597
+ for (const subscriber of endpoint.subscribers.values()) {
1598
+ try {
1599
+ const filteredData = filterToSelection(data, subscriber.selection);
1600
+ subscriber.onData(filteredData);
1601
+ } catch (error) {
1602
+ if (subscriber.onError) {
1603
+ subscriber.onError(error instanceof Error ? error : new Error(String(error)));
1604
+ }
1605
+ }
1606
+ }
1607
+ }
1608
+ distributeError(endpointKey, error) {
1609
+ const endpoint = this.endpoints.get(endpointKey);
1610
+ if (!endpoint)
1611
+ return;
1612
+ for (const subscriber of endpoint.subscribers.values()) {
1613
+ subscriber.onError?.(error);
1614
+ }
1615
+ }
1616
+ getLastData(endpointKey) {
1617
+ return this.endpoints.get(endpointKey)?.lastData ?? null;
1618
+ }
1619
+ markSubscribed(endpointKey) {
1620
+ const endpoint = this.endpoints.get(endpointKey);
1621
+ if (endpoint) {
1622
+ endpoint.isSubscribed = true;
1623
+ }
1624
+ }
1625
+ markUnsubscribed(endpointKey) {
1626
+ const endpoint = this.endpoints.get(endpointKey);
1627
+ if (endpoint) {
1628
+ endpoint.isSubscribed = false;
1629
+ }
1630
+ }
1631
+ isSubscribed(endpointKey) {
1632
+ return this.endpoints.get(endpointKey)?.isSubscribed ?? false;
1633
+ }
1634
+ getEndpointKeys() {
1635
+ return Array.from(this.endpoints.keys());
1636
+ }
1637
+ clear() {
1638
+ this.endpoints.clear();
1639
+ }
1640
+ getStats() {
1641
+ let totalSubscribers = 0;
1642
+ for (const endpoint of this.endpoints.values()) {
1643
+ totalSubscribers += endpoint.subscribers.size;
1644
+ }
1645
+ return {
1646
+ endpointCount: this.endpoints.size,
1647
+ totalSubscribers,
1648
+ avgSubscribersPerEndpoint: this.endpoints.size > 0 ? totalSubscribers / this.endpoints.size : 0
1649
+ };
1650
+ }
1651
+ computeMergedSelection(endpoint) {
1652
+ const selections = Array.from(endpoint.subscribers.values()).map((s) => s.selection);
1653
+ return mergeSelections(selections);
1654
+ }
1655
+ analyzeSelectionChange(previous, next) {
1656
+ const previousFields = this.flattenSelectionKeys(previous);
1657
+ const nextFields = this.flattenSelectionKeys(next);
1658
+ const addedFields = new Set;
1659
+ const removedFields = new Set;
1660
+ for (const field of nextFields) {
1661
+ if (!previousFields.has(field)) {
1662
+ addedFields.add(field);
1663
+ }
1664
+ }
1665
+ for (const field of previousFields) {
1666
+ if (!nextFields.has(field)) {
1667
+ removedFields.add(field);
1668
+ }
1669
+ }
1670
+ const hasChanged = addedFields.size > 0 || removedFields.size > 0;
1671
+ return {
1672
+ hasChanged,
1673
+ previousSelection: previous,
1674
+ newSelection: next,
1675
+ addedFields,
1676
+ removedFields,
1677
+ isExpanded: addedFields.size > 0,
1678
+ isShrunk: removedFields.size > 0
1679
+ };
1680
+ }
1681
+ flattenSelectionKeys(selection, prefix = "") {
1682
+ const keys = new Set;
1683
+ for (const [key, value] of Object.entries(selection)) {
1684
+ const path = prefix ? `${prefix}.${key}` : key;
1685
+ keys.add(path);
1686
+ if (typeof value === "boolean") {
1687
+ continue;
1688
+ }
1689
+ if (typeof value === "object" && value !== null) {
1690
+ let nestedSelection;
1691
+ if ("select" in value && typeof value.select === "object") {
1692
+ nestedSelection = value.select;
1693
+ } else if (!("input" in value)) {
1694
+ nestedSelection = value;
1695
+ }
1696
+ if (nestedSelection) {
1697
+ const nestedKeys = this.flattenSelectionKeys(nestedSelection, path);
1698
+ for (const nestedKey of nestedKeys) {
1699
+ keys.add(nestedKey);
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+ return keys;
1705
+ }
1706
+ noChangeAnalysis() {
1707
+ return {
1708
+ hasChanged: false,
1709
+ previousSelection: {},
1710
+ newSelection: {},
1711
+ addedFields: new Set,
1712
+ removedFields: new Set,
1713
+ isExpanded: false,
1714
+ isShrunk: false
1715
+ };
1716
+ }
1717
+ }
1718
+ function mergeSelections(selections) {
1719
+ if (selections.length === 0)
1720
+ return {};
1721
+ if (selections.length === 1)
1722
+ return selections[0];
1723
+ const merged = {};
1724
+ const allKeys = new Set;
1725
+ for (const selection of selections) {
1726
+ for (const key of Object.keys(selection)) {
1727
+ allKeys.add(key);
1728
+ }
1729
+ }
1730
+ for (const key of allKeys) {
1731
+ const values = selections.map((s) => s[key]).filter((v) => v !== undefined && v !== null);
1732
+ if (values.length === 0)
1733
+ continue;
1734
+ if (values.some((v) => v === true)) {
1735
+ merged[key] = true;
1736
+ continue;
1737
+ }
1738
+ const nestedSelections = [];
1739
+ let lastInput;
1740
+ for (const value of values) {
1741
+ if (typeof value === "object" && value !== null) {
1742
+ if ("select" in value && typeof value.select === "object") {
1743
+ nestedSelections.push(value.select);
1744
+ if ("input" in value) {
1745
+ lastInput = value.input;
1746
+ }
1747
+ } else if ("input" in value) {
1748
+ lastInput = value.input;
1749
+ merged[key] = { input: lastInput };
1750
+ } else {
1751
+ nestedSelections.push(value);
1752
+ }
1753
+ }
1754
+ }
1755
+ if (nestedSelections.length > 0) {
1756
+ const mergedNested = mergeSelections(nestedSelections);
1757
+ if (lastInput !== undefined) {
1758
+ merged[key] = { input: lastInput, select: mergedNested };
1759
+ } else {
1760
+ merged[key] = mergedNested;
1761
+ }
1762
+ }
1763
+ }
1764
+ return merged;
1765
+ }
1766
+ function filterToSelection(data, selection) {
1767
+ if (data == null)
1768
+ return data;
1769
+ if (Array.isArray(data)) {
1770
+ return data.map((item) => filterToSelection(item, selection));
1771
+ }
1772
+ if (typeof data !== "object")
1773
+ return data;
1774
+ const obj = data;
1775
+ const result = {};
1776
+ if ("id" in obj) {
1777
+ result.id = obj.id;
1778
+ }
1779
+ for (const [key, value] of Object.entries(selection)) {
1780
+ if (!(key in obj))
1781
+ continue;
1782
+ if (value === true) {
1783
+ result[key] = obj[key];
1784
+ } else if (typeof value === "object" && value !== null) {
1785
+ let nestedSelection = null;
1786
+ if ("select" in value && typeof value.select === "object") {
1787
+ nestedSelection = value.select;
1788
+ } else if (!("input" in value)) {
1789
+ nestedSelection = value;
1790
+ }
1791
+ if (nestedSelection) {
1792
+ result[key] = filterToSelection(obj[key], nestedSelection);
1793
+ } else {
1794
+ result[key] = obj[key];
1795
+ }
1796
+ }
1797
+ }
1798
+ return result;
1799
+ }
1313
1800
  function hasAnySubscription(entities, entityName, select, visited = new Set) {
1314
1801
  if (!entities)
1315
1802
  return false;
@@ -1341,13 +1828,19 @@ function hasAnySubscription(entities, entityName, select, visited = new Set) {
1341
1828
  }
1342
1829
  return false;
1343
1830
  }
1831
+ var subscriberIdCounter = 0;
1832
+ function generateSubscriberId() {
1833
+ return `sub_${Date.now()}_${++subscriberIdCounter}`;
1834
+ }
1344
1835
 
1345
1836
  class ClientImpl {
1346
1837
  transport;
1347
1838
  plugins;
1348
1839
  metadata = null;
1349
1840
  connectPromise = null;
1350
- subscriptions = new Map;
1841
+ endpoints = new Map;
1842
+ pendingBatches = new Map;
1843
+ batchScheduled = false;
1351
1844
  queryResultCache = new Map;
1352
1845
  observerEntries = new WeakMap;
1353
1846
  constructor(config) {
@@ -1463,7 +1956,7 @@ class ClientImpl {
1463
1956
  return `${type}-${path}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1464
1957
  }
1465
1958
  inputHashCache = new WeakMap;
1466
- makeQueryKey(path, input) {
1959
+ makeEndpointKey(path, input) {
1467
1960
  if (input === undefined || input === null) {
1468
1961
  return `${path}:null`;
1469
1962
  }
@@ -1478,69 +1971,231 @@ class ClientImpl {
1478
1971
  }
1479
1972
  return `${path}:${hash}`;
1480
1973
  }
1481
- executeQuery(path, input, select) {
1482
- const key = this.makeQueryKey(path, input);
1483
- const cached = this.queryResultCache.get(key);
1484
- if (cached && !select) {
1485
- return cached;
1486
- }
1487
- if (!this.subscriptions.has(key)) {
1488
- this.subscriptions.set(key, {
1974
+ makeQueryResultKey(endpointKey, select) {
1975
+ if (!select)
1976
+ return endpointKey;
1977
+ return `${endpointKey}:${JSON.stringify(select)}`;
1978
+ }
1979
+ getOrCreateEndpoint(key) {
1980
+ let endpoint = this.endpoints.get(key);
1981
+ if (!endpoint) {
1982
+ endpoint = {
1489
1983
  data: null,
1490
1984
  error: null,
1491
1985
  completed: false,
1492
- observers: new Set,
1493
- ...select && { select }
1494
- });
1986
+ observers: new Map,
1987
+ mergedSelection: undefined,
1988
+ isSubscribed: false
1989
+ };
1990
+ this.endpoints.set(key, endpoint);
1991
+ }
1992
+ return endpoint;
1993
+ }
1994
+ addObserver(key, observer) {
1995
+ const endpoint = this.getOrCreateEndpoint(key);
1996
+ const previousSelection = endpoint.mergedSelection;
1997
+ endpoint.observers.set(observer.id, observer);
1998
+ const selections = Array.from(endpoint.observers.values()).map((o) => o.selection).filter((s) => s !== undefined);
1999
+ endpoint.mergedSelection = selections.length > 0 ? mergeSelections(selections) : undefined;
2000
+ const selectionChanged = JSON.stringify(previousSelection) !== JSON.stringify(endpoint.mergedSelection);
2001
+ const isExpanded = selectionChanged && this.isSelectionExpanded(previousSelection, endpoint.mergedSelection);
2002
+ return { endpoint, selectionChanged, isExpanded };
2003
+ }
2004
+ removeObserver(key, observerId) {
2005
+ const endpoint = this.endpoints.get(key);
2006
+ if (!endpoint)
2007
+ return { endpoint: undefined, shouldUnsubscribe: false };
2008
+ endpoint.observers.delete(observerId);
2009
+ if (endpoint.observers.size === 0) {
2010
+ return { endpoint, shouldUnsubscribe: true };
2011
+ }
2012
+ const selections = Array.from(endpoint.observers.values()).map((o) => o.selection).filter((s) => s !== undefined);
2013
+ endpoint.mergedSelection = selections.length > 0 ? mergeSelections(selections) : undefined;
2014
+ return { endpoint, shouldUnsubscribe: false };
2015
+ }
2016
+ isSelectionExpanded(previous, current) {
2017
+ if (!previous)
2018
+ return current !== undefined;
2019
+ if (!current)
2020
+ return false;
2021
+ const previousKeys = this.flattenSelectionKeys(previous);
2022
+ const currentKeys = this.flattenSelectionKeys(current);
2023
+ for (const key of currentKeys) {
2024
+ if (!previousKeys.has(key))
2025
+ return true;
2026
+ }
2027
+ return false;
2028
+ }
2029
+ flattenSelectionKeys(selection, prefix = "") {
2030
+ const keys = new Set;
2031
+ for (const [key, value] of Object.entries(selection)) {
2032
+ const path = prefix ? `${prefix}.${key}` : key;
2033
+ keys.add(path);
2034
+ if (typeof value === "boolean") {
2035
+ continue;
2036
+ }
2037
+ if (typeof value === "object" && value !== null) {
2038
+ const nested = "select" in value ? value.select : value;
2039
+ if (nested && typeof nested === "object") {
2040
+ for (const nestedKey of this.flattenSelectionKeys(nested, path)) {
2041
+ keys.add(nestedKey);
2042
+ }
2043
+ }
2044
+ }
2045
+ }
2046
+ return keys;
2047
+ }
2048
+ distributeData(endpoint, data) {
2049
+ endpoint.data = data;
2050
+ endpoint.error = null;
2051
+ for (const observer of endpoint.observers.values()) {
2052
+ if (observer.next) {
2053
+ const filteredData = observer.selection ? filterToSelection(data, observer.selection) : data;
2054
+ observer.next(filteredData);
2055
+ }
2056
+ }
2057
+ }
2058
+ distributeError(endpoint, error) {
2059
+ endpoint.error = error;
2060
+ for (const observer of endpoint.observers.values()) {
2061
+ observer.error?.(error);
1495
2062
  }
1496
- const sub = this.subscriptions.get(key);
2063
+ }
2064
+ scheduleBatchedQuery(key, path, input, selection) {
2065
+ return new Promise((resolve, reject) => {
2066
+ const observerId = generateSubscriberId();
2067
+ let batch = this.pendingBatches.get(key);
2068
+ if (!batch) {
2069
+ batch = {
2070
+ path,
2071
+ input,
2072
+ observers: [],
2073
+ mergedSelection: undefined
2074
+ };
2075
+ this.pendingBatches.set(key, batch);
2076
+ }
2077
+ batch.observers.push({ id: observerId, selection, resolve, reject });
2078
+ const selections = batch.observers.map((o) => o.selection).filter((s) => s !== undefined);
2079
+ batch.mergedSelection = selections.length > 0 ? mergeSelections(selections) : undefined;
2080
+ if (!this.batchScheduled) {
2081
+ this.batchScheduled = true;
2082
+ queueMicrotask(() => this.flushBatches());
2083
+ }
2084
+ });
2085
+ }
2086
+ async flushBatches() {
2087
+ this.batchScheduled = false;
2088
+ const batches = Array.from(this.pendingBatches.entries());
2089
+ this.pendingBatches.clear();
2090
+ await Promise.all(batches.map(async ([key, batch]) => {
2091
+ try {
2092
+ const op2 = {
2093
+ id: this.generateId("query", batch.path),
2094
+ path: batch.path,
2095
+ type: "query",
2096
+ input: batch.input,
2097
+ meta: batch.mergedSelection ? { select: batch.mergedSelection } : {}
2098
+ };
2099
+ const response = await this.execute(op2);
2100
+ if (isError(response)) {
2101
+ const error = new Error(response.error);
2102
+ for (const observer of batch.observers) {
2103
+ observer.reject(error);
2104
+ }
2105
+ return;
2106
+ }
2107
+ if (isSnapshot(response)) {
2108
+ const endpoint = this.getOrCreateEndpoint(key);
2109
+ endpoint.data = response.data;
2110
+ for (const observer of batch.observers) {
2111
+ const filteredData = observer.selection ? filterToSelection(response.data, observer.selection) : response.data;
2112
+ observer.resolve(filteredData);
2113
+ }
2114
+ }
2115
+ } catch (error) {
2116
+ const err = error instanceof Error ? error : new Error(String(error));
2117
+ for (const observer of batch.observers) {
2118
+ observer.reject(err);
2119
+ }
2120
+ }
2121
+ }));
2122
+ }
2123
+ executeQuery(path, input, select) {
2124
+ const key = this.makeEndpointKey(path, input);
2125
+ const cacheKey = this.makeQueryResultKey(key, select);
2126
+ const cached = this.queryResultCache.get(cacheKey);
2127
+ if (cached) {
2128
+ return cached;
2129
+ }
2130
+ const endpoint = this.getOrCreateEndpoint(key);
1497
2131
  const result = {
1498
2132
  get value() {
1499
- return sub.data;
2133
+ if (endpoint.data === null)
2134
+ return null;
2135
+ return select ? filterToSelection(endpoint.data, select) : endpoint.data;
1500
2136
  },
1501
2137
  subscribe: (observerOrCallback) => {
2138
+ const observerId = generateSubscriberId();
1502
2139
  let entry;
1503
2140
  if (typeof observerOrCallback === "function") {
1504
2141
  const callback = observerOrCallback;
1505
- entry = { next: (data) => callback(data) };
2142
+ entry = {
2143
+ id: observerId,
2144
+ selection: select,
2145
+ next: (data) => callback(data)
2146
+ };
1506
2147
  } else if (observerOrCallback && typeof observerOrCallback === "object") {
1507
2148
  const observer = observerOrCallback;
1508
2149
  entry = {
2150
+ id: observerId,
2151
+ selection: select,
1509
2152
  next: observer.next ? (data) => observer.next(data) : undefined,
1510
2153
  error: observer.error,
1511
2154
  complete: observer.complete
1512
2155
  };
1513
2156
  } else {
1514
- entry = {};
2157
+ entry = { id: observerId, selection: select };
1515
2158
  }
1516
2159
  if (observerOrCallback) {
1517
2160
  this.observerEntries.set(observerOrCallback, entry);
1518
2161
  }
1519
- sub.observers.add(entry);
1520
- if (sub.error && entry.error) {
1521
- entry.error(sub.error);
1522
- } else if (sub.data !== null && entry.next) {
1523
- entry.next(sub.data);
1524
- }
1525
- if (sub.completed && entry.complete) {
1526
- entry.complete();
1527
- }
1528
- if (!sub.unsubscribe) {
2162
+ const { endpoint: ep, isExpanded } = this.addObserver(key, entry);
2163
+ if (!ep.isSubscribed) {
1529
2164
  this.startSubscription(path, input, key);
2165
+ } else if (isExpanded) {
2166
+ if (ep.unsubscribe) {
2167
+ ep.unsubscribe();
2168
+ }
2169
+ ep.isSubscribed = false;
2170
+ this.startSubscription(path, input, key);
2171
+ } else {
2172
+ if (ep.error && entry.error) {
2173
+ entry.error(ep.error);
2174
+ } else if (ep.data !== null && entry.next) {
2175
+ const filteredData = select ? filterToSelection(ep.data, select) : ep.data;
2176
+ entry.next(filteredData);
2177
+ }
2178
+ if (ep.completed && entry.complete) {
2179
+ entry.complete();
2180
+ }
1530
2181
  }
1531
2182
  return () => {
1532
2183
  if (observerOrCallback) {
1533
2184
  const storedEntry = this.observerEntries.get(observerOrCallback);
1534
2185
  if (storedEntry) {
1535
- sub.observers.delete(storedEntry);
2186
+ const { shouldUnsubscribe } = this.removeObserver(key, storedEntry.id);
2187
+ if (shouldUnsubscribe) {
2188
+ ep.unsubscribe?.();
2189
+ ep.isSubscribed = false;
2190
+ this.endpoints.delete(key);
2191
+ for (const [k] of this.queryResultCache) {
2192
+ if (k.startsWith(key)) {
2193
+ this.queryResultCache.delete(k);
2194
+ }
2195
+ }
2196
+ }
1536
2197
  }
1537
2198
  }
1538
- if (sub.observers.size === 0 && sub.unsubscribe) {
1539
- sub.unsubscribe();
1540
- sub.unsubscribe = undefined;
1541
- this.subscriptions.delete(key);
1542
- this.queryResultCache.delete(key);
1543
- }
1544
2199
  };
1545
2200
  },
1546
2201
  select: (selection) => {
@@ -1548,30 +2203,17 @@ class ClientImpl {
1548
2203
  },
1549
2204
  then: async (onfulfilled, onrejected) => {
1550
2205
  try {
1551
- const op2 = {
1552
- id: this.generateId("query", path),
1553
- path,
1554
- type: "query",
1555
- input,
1556
- meta: select ? { select } : {}
1557
- };
1558
- const response = await this.execute(op2);
1559
- if (isError(response)) {
1560
- throw new Error(response.error);
1561
- }
1562
- if (isSnapshot(response)) {
1563
- sub.data = response.data;
1564
- for (const observer of sub.observers) {
1565
- observer.next?.(response.data);
1566
- }
1567
- return onfulfilled ? onfulfilled(response.data) : response.data;
2206
+ const data = await this.scheduleBatchedQuery(key, path, input, select);
2207
+ const ep = this.getOrCreateEndpoint(key);
2208
+ if (ep.data === null) {
2209
+ ep.data = data;
1568
2210
  }
1569
- return onfulfilled ? onfulfilled(sub.data) : sub.data;
2211
+ return onfulfilled ? onfulfilled(data) : data;
1570
2212
  } catch (error) {
1571
2213
  const err = error instanceof Error ? error : new Error(String(error));
1572
- sub.error = err;
1573
- for (const observer of sub.observers) {
1574
- observer.error?.(err);
2214
+ const ep = this.endpoints.get(key);
2215
+ if (ep) {
2216
+ ep.error = err;
1575
2217
  }
1576
2218
  if (onrejected) {
1577
2219
  return onrejected(error);
@@ -1580,78 +2222,70 @@ class ClientImpl {
1580
2222
  }
1581
2223
  }
1582
2224
  };
1583
- if (!select) {
1584
- this.queryResultCache.set(key, result);
1585
- }
2225
+ this.queryResultCache.set(cacheKey, result);
1586
2226
  return result;
1587
2227
  }
1588
2228
  async startSubscription(path, input, key) {
1589
- const sub = this.subscriptions.get(key);
1590
- if (!sub)
2229
+ const endpoint = this.endpoints.get(key);
2230
+ if (!endpoint)
1591
2231
  return;
1592
2232
  await this.ensureConnected();
1593
- const isSubscription = this.requiresSubscription(path, sub.select);
2233
+ const meta = this.getOperationMeta(path);
2234
+ if (meta?.type === "mutation") {
2235
+ return;
2236
+ }
2237
+ const isSubscription = this.requiresSubscription(path, endpoint.mergedSelection);
2238
+ endpoint.isSubscribed = true;
1594
2239
  if (isSubscription) {
1595
2240
  const op2 = {
1596
2241
  id: this.generateId("subscription", path),
1597
2242
  path,
1598
2243
  type: "subscription",
1599
- input
2244
+ input,
2245
+ meta: endpoint.mergedSelection ? { select: endpoint.mergedSelection } : {}
1600
2246
  };
1601
2247
  const resultOrObservable = this.transport.execute(op2);
1602
2248
  if (this.isObservable(resultOrObservable)) {
1603
2249
  const subscription = resultOrObservable.subscribe({
1604
2250
  next: (message) => {
1605
2251
  if (isSnapshot(message)) {
1606
- sub.data = message.data;
1607
- sub.error = null;
1608
- for (const observer of sub.observers) {
1609
- observer.next?.(message.data);
1610
- }
2252
+ this.distributeData(endpoint, message.data);
1611
2253
  } else if (isOps(message)) {
1612
2254
  try {
1613
- sub.data = applyOps(sub.data, message.ops);
1614
- sub.error = null;
1615
- for (const observer of sub.observers) {
1616
- observer.next?.(sub.data);
1617
- }
2255
+ const newData = applyOps(endpoint.data, message.ops);
2256
+ this.distributeData(endpoint, newData);
1618
2257
  } catch (updateErr) {
1619
2258
  const err = updateErr instanceof Error ? updateErr : new Error(String(updateErr));
1620
- sub.error = err;
1621
- for (const observer of sub.observers) {
1622
- observer.error?.(err);
1623
- }
2259
+ this.distributeError(endpoint, err);
1624
2260
  }
1625
2261
  } else if (isError(message)) {
1626
- const err = new Error(message.error);
1627
- sub.error = err;
1628
- for (const observer of sub.observers) {
1629
- observer.error?.(err);
1630
- }
2262
+ this.distributeError(endpoint, new Error(message.error));
1631
2263
  }
1632
2264
  },
1633
2265
  error: (err) => {
1634
- sub.error = err;
1635
- for (const observer of sub.observers) {
1636
- observer.error?.(err);
1637
- }
2266
+ this.distributeError(endpoint, err);
1638
2267
  },
1639
2268
  complete: () => {
1640
- sub.completed = true;
1641
- for (const observer of sub.observers) {
2269
+ endpoint.completed = true;
2270
+ for (const observer of endpoint.observers.values()) {
1642
2271
  observer.complete?.();
1643
2272
  }
1644
2273
  }
1645
2274
  });
1646
- sub.unsubscribe = () => subscription.unsubscribe();
2275
+ endpoint.unsubscribe = () => subscription.unsubscribe();
1647
2276
  }
1648
2277
  } else {
1649
- this.executeQuery(path, input).then(() => {
1650
- sub.completed = true;
1651
- for (const observer of sub.observers) {
2278
+ try {
2279
+ const data = await this.scheduleBatchedQuery(key, path, input, endpoint.mergedSelection);
2280
+ this.distributeData(endpoint, data);
2281
+ endpoint.completed = true;
2282
+ for (const observer of endpoint.observers.values()) {
1652
2283
  observer.complete?.();
1653
2284
  }
1654
- });
2285
+ } catch (error) {
2286
+ const err = error instanceof Error ? error : new Error(String(error));
2287
+ this.distributeError(endpoint, err);
2288
+ }
1655
2289
  }
1656
2290
  }
1657
2291
  async executeMutation(path, input, select) {
@@ -1699,6 +2333,17 @@ class ClientImpl {
1699
2333
  return queryResult;
1700
2334
  };
1701
2335
  }
2336
+ getStats() {
2337
+ let totalObservers = 0;
2338
+ for (const endpoint of this.endpoints.values()) {
2339
+ totalObservers += endpoint.observers.size;
2340
+ }
2341
+ return {
2342
+ endpointCount: this.endpoints.size,
2343
+ totalObservers,
2344
+ pendingBatches: this.pendingBatches.size
2345
+ };
2346
+ }
1702
2347
  }
1703
2348
  function createClient(config) {
1704
2349
  const impl = new ClientImpl(config);
@@ -1883,217 +2528,6 @@ http.server = function httpServer(options) {
1883
2528
  }
1884
2529
  };
1885
2530
  };
1886
- class SubscriptionRegistry {
1887
- subscriptions = new Map;
1888
- entityIndex = new Map;
1889
- add(sub) {
1890
- const tracked = {
1891
- ...sub,
1892
- state: "pending",
1893
- lastDataHash: sub.lastData ? hashEntityState(sub.lastData) : null,
1894
- createdAt: Date.now(),
1895
- lastUpdateAt: null
1896
- };
1897
- this.subscriptions.set(sub.id, tracked);
1898
- const entityKey = `${sub.entity}:${sub.entityId}`;
1899
- let ids = this.entityIndex.get(entityKey);
1900
- if (!ids) {
1901
- ids = new Set;
1902
- this.entityIndex.set(entityKey, ids);
1903
- }
1904
- ids.add(sub.id);
1905
- }
1906
- get(id) {
1907
- return this.subscriptions.get(id);
1908
- }
1909
- has(id) {
1910
- return this.subscriptions.has(id);
1911
- }
1912
- remove(id) {
1913
- const sub = this.subscriptions.get(id);
1914
- if (!sub)
1915
- return;
1916
- this.subscriptions.delete(id);
1917
- const entityKey = `${sub.entity}:${sub.entityId}`;
1918
- const ids = this.entityIndex.get(entityKey);
1919
- if (ids) {
1920
- ids.delete(id);
1921
- if (ids.size === 0) {
1922
- this.entityIndex.delete(entityKey);
1923
- }
1924
- }
1925
- }
1926
- getByEntity(entity3, entityId) {
1927
- const entityKey = `${entity3}:${entityId}`;
1928
- const ids = this.entityIndex.get(entityKey);
1929
- if (!ids)
1930
- return [];
1931
- const result = [];
1932
- for (const id of ids) {
1933
- const sub = this.subscriptions.get(id);
1934
- if (sub) {
1935
- result.push(sub);
1936
- }
1937
- }
1938
- return result;
1939
- }
1940
- updateVersion(id, version, data) {
1941
- const sub = this.subscriptions.get(id);
1942
- if (!sub)
1943
- return;
1944
- sub.version = version;
1945
- sub.lastUpdateAt = Date.now();
1946
- if (data !== undefined) {
1947
- sub.lastData = data;
1948
- sub.lastDataHash = hashEntityState(data);
1949
- }
1950
- if (sub.state === "pending" || sub.state === "reconnecting") {
1951
- sub.state = "active";
1952
- }
1953
- }
1954
- updateData(id, data) {
1955
- const sub = this.subscriptions.get(id);
1956
- if (!sub)
1957
- return;
1958
- sub.lastData = data;
1959
- sub.lastDataHash = hashEntityState(data);
1960
- }
1961
- getLastData(id) {
1962
- return this.subscriptions.get(id)?.lastData ?? null;
1963
- }
1964
- getVersion(id) {
1965
- return this.subscriptions.get(id)?.version ?? null;
1966
- }
1967
- markActive(id) {
1968
- const sub = this.subscriptions.get(id);
1969
- if (sub) {
1970
- sub.state = "active";
1971
- }
1972
- }
1973
- markError(id) {
1974
- const sub = this.subscriptions.get(id);
1975
- if (sub) {
1976
- sub.state = "error";
1977
- }
1978
- }
1979
- markAllReconnecting() {
1980
- for (const sub of this.subscriptions.values()) {
1981
- if (sub.state === "active") {
1982
- sub.state = "reconnecting";
1983
- }
1984
- }
1985
- }
1986
- getByState(state) {
1987
- const result = [];
1988
- for (const sub of this.subscriptions.values()) {
1989
- if (sub.state === state) {
1990
- result.push(sub);
1991
- }
1992
- }
1993
- return result;
1994
- }
1995
- getAllForReconnect() {
1996
- const result = [];
1997
- for (const sub of this.subscriptions.values()) {
1998
- if (sub.state === "reconnecting" || sub.state === "active") {
1999
- const reconnectSub = {
2000
- id: sub.id,
2001
- entity: sub.entity,
2002
- entityId: sub.entityId,
2003
- fields: sub.fields,
2004
- version: sub.version,
2005
- input: sub.input
2006
- };
2007
- if (sub.lastDataHash) {
2008
- reconnectSub.dataHash = sub.lastDataHash;
2009
- }
2010
- result.push(reconnectSub);
2011
- }
2012
- }
2013
- return result;
2014
- }
2015
- processReconnectResult(id, version, data) {
2016
- const sub = this.subscriptions.get(id);
2017
- if (!sub)
2018
- return;
2019
- sub.version = version;
2020
- sub.state = "active";
2021
- sub.lastUpdateAt = Date.now();
2022
- if (data !== undefined) {
2023
- sub.lastData = data;
2024
- sub.lastDataHash = hashEntityState(data);
2025
- }
2026
- }
2027
- getObserver(id) {
2028
- return this.subscriptions.get(id)?.observer;
2029
- }
2030
- updateObserver(id, observer) {
2031
- const sub = this.subscriptions.get(id);
2032
- if (sub) {
2033
- sub.observer = observer;
2034
- }
2035
- }
2036
- notifyNext(id, data) {
2037
- const sub = this.subscriptions.get(id);
2038
- sub?.observer.next?.({ data, version: sub.version });
2039
- }
2040
- notifyError(id, error) {
2041
- this.subscriptions.get(id)?.observer.error?.(error);
2042
- }
2043
- notifyAllReconnectingError(error) {
2044
- for (const sub of this.subscriptions.values()) {
2045
- if (sub.state === "reconnecting") {
2046
- sub.observer.error?.(error);
2047
- }
2048
- }
2049
- }
2050
- get size() {
2051
- return this.subscriptions.size;
2052
- }
2053
- getIds() {
2054
- return Array.from(this.subscriptions.keys());
2055
- }
2056
- values() {
2057
- return this.subscriptions.values();
2058
- }
2059
- getStats() {
2060
- const byState = {
2061
- pending: 0,
2062
- active: 0,
2063
- reconnecting: 0,
2064
- error: 0
2065
- };
2066
- const byEntity = {};
2067
- for (const sub of this.subscriptions.values()) {
2068
- byState[sub.state]++;
2069
- const entityKey = `${sub.entity}:${sub.entityId}`;
2070
- byEntity[entityKey] = (byEntity[entityKey] ?? 0) + 1;
2071
- }
2072
- return {
2073
- total: this.subscriptions.size,
2074
- byState,
2075
- byEntity
2076
- };
2077
- }
2078
- clear() {
2079
- for (const sub of this.subscriptions.values()) {
2080
- sub.observer.complete?.();
2081
- }
2082
- this.subscriptions.clear();
2083
- this.entityIndex.clear();
2084
- }
2085
- clearErrors() {
2086
- const toRemove = [];
2087
- for (const [id, sub] of this.subscriptions) {
2088
- if (sub.state === "error") {
2089
- toRemove.push(id);
2090
- }
2091
- }
2092
- for (const id of toRemove) {
2093
- this.remove(id);
2094
- }
2095
- }
2096
- }
2097
2531
 
2098
2532
  // src/create.ts
2099
2533
  import { createEffect, createSignal, on, onCleanup } from "solid-js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-solid",
3
- "version": "2.3.4",
3
+ "version": "2.3.5",
4
4
  "description": "SolidJS bindings for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,8 +31,8 @@
31
31
  "author": "SylphxAI",
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
- "@sylphx/lens-client": "^2.6.0",
35
- "@sylphx/lens-core": "^2.12.0"
34
+ "@sylphx/lens-client": "^2.6.1",
35
+ "@sylphx/lens-core": "^2.12.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "solid-js": ">=1.8.0"