coll-fns 1.4.2 → 1.5.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/README.md CHANGED
@@ -13,6 +13,7 @@ Stop repeating business logic all over your code base. Define hooks on collectio
13
13
  - [Rationale](#rationale)
14
14
  - [Installation and configuration](#installation-and-configuration)
15
15
  - [`setProtocol(protocol)`](#setprotocolprotocol)
16
+ - [Execution context (`bindEnvironment`)](#execution-context-bindenvironment)
16
17
  - [Bypassing `coll-fns`](#bypassing-coll-fns)
17
18
  - [Joins and fetch](#joins-and-fetch)
18
19
  - [Quick start examples](#quick-start-examples)
@@ -53,6 +54,17 @@ Stop repeating business logic all over your code base. Define hooks on collectio
53
54
  - [Default behavior](#default-behavior)
54
55
  - [Options](#options)
55
56
  - [Hook best practices](#hook-best-practices)
57
+ - [Nested reactive publications](#nested-reactive-publications)
58
+ - [When to use `publish`](#when-to-use-publish)
59
+ - [`publish(publication, Coll, selector, options)`](#publishpublication-coll-selector-options)
60
+ - [Publication context (`this` in Meteor)](#publication-context-this-in-meteor)
61
+ - [How child declarations work](#how-child-declarations-work)
62
+ - [Ancestors chain](#ancestors-chain)
63
+ - [How `deps` works](#how-deps-works)
64
+ - [Debugging](#debugging)
65
+ - [Built-in optimizations](#built-in-optimizations)
66
+ - [Using `publish` outside Meteor](#using-publish-outside-meteor)
67
+ - [Current limitations](#current-limitations)
56
68
  - [License](#license)
57
69
 
58
70
  # Rationale
@@ -122,7 +134,7 @@ And since it uses a protocol, it can be used with the **native MongoDB driver**
122
134
 
123
135
  </details>
124
136
 
125
- <details style="margin-bottom: 1rem">
137
+ <details>
126
138
  <summary><strong>Functional API</strong></summary>
127
139
 
128
140
  A lot of libraries that add functionalities to the database layer mutate the collection instances themselves or, when more respectful, offer ways to extend the collection constructor somehow.
@@ -139,6 +151,26 @@ For Meteor developers, it also means being able to enhance the `Meteor.users` co
139
151
 
140
152
  </details>
141
153
 
154
+ <details style="margin-bottom: 1rem">
155
+ <summary><strong>Nested reactive publications</strong></summary>
156
+
157
+ In Meteor in particular, publication of data over DDP that gets synced on the client in Minimongo is very convenient. It makes for really reactive applications where data doesn't get stale. Optimistic UI optimizations is built-in and doesn't require additional complex logic.
158
+
159
+ However, it can get **difficult to publish exactly the right documents**, especially when using a very useful library that allows joining collections together and keeping data much more normalized!
160
+
161
+ Although returning Meteor cursors from a publish function is still the most optimized path, it sometimes makes sense to **publish children documents based on their relationship with their parent**. But doing so is usually very difficult to implement.
162
+
163
+ Some existing community libraries that promise to do so either:
164
+
165
+ - don't actually work (might understand `added` and `removed` callbacks, but won't react to updates)
166
+ - are too simplistic (cause a lot of duplicated observers)
167
+ - come with a much more complex data layer
168
+ - add somewhat unpredictable reactivity on the server.
169
+
170
+ Since `coll-fns` is already very good at understanding relationships, a `publish` helper was introduced to allow such fine-tuned reactivity out of the box. 💪
171
+
172
+ </details>
173
+
142
174
  # Installation and configuration
143
175
 
144
176
  **IMPORTANT**: For concision, the **examples will use the synchronous Meteor protocol** to avoid `async/await` boilerplate. Of course, your code will have to be adapted when used with an asynchronous protocol.
@@ -174,6 +206,22 @@ setProtocol(protocol);
174
206
 
175
207
  There's also a **native NodeJS MongoDB driver** protocol built-in (`protocols.node`).
176
208
 
209
+ ### Execution context (`bindEnvironment`)
210
+
211
+ Protocols can optionally expose a `bindEnvironment(fn)` method.
212
+ It is used by hooks internals to run callbacks in the expected runtime context.
213
+
214
+ - In Meteor Fiber-based servers, this prevents errors like:
215
+ - `Meteor code must always run within a Fiber...`
216
+ - In environments that don't need special wrapping, it can be omitted.
217
+
218
+ `protocols.meteorSync` already handles this automatically:
219
+
220
+ - On Meteor server with Fibers, it uses `Meteor.bindEnvironment` when available.
221
+ - On Meteor client (or any environment where `Meteor.bindEnvironment` is unavailable), it falls back to a normal direct call.
222
+
223
+ So if you use `protocols.meteorSync` on both client and server in a Fiber-based Meteor app, **no override is required** specifically for this behavior.
224
+
177
225
  <details style="margin-bottom: 1rem">
178
226
  <summary><strong>Custom protocol</strong></summary>
179
227
 
@@ -196,14 +244,27 @@ const customProtocol = {
196
244
  * after being fetched with descendants. */
197
245
  getTransform(/* Coll */) {},
198
246
 
247
+ /* Optional. Wrap callbacks to preserve runtime context
248
+ * (ex: Meteor.bindEnvironment with Fibers). */
249
+ bindEnvironment(/* fn */) {},
250
+
199
251
  /* Insert a document in a collection
200
252
  * and return the inserted _id. */
201
253
  insert(/* Coll, doc, options */) {},
202
254
 
255
+ /* Observe document changes.
256
+ * Must return a handle with a `stop()` method. */
257
+ observe(/* Coll, selector = {}, callbacks = {}, options = {} */) {},
258
+
203
259
  /* Remove documents in a collection
204
260
  * and return the number of removed documents. */
205
261
  remove(/* Coll, selector, options */) {},
206
262
 
263
+ /* Stable stringify function used for internal query keys.
264
+ * EJSON canonical stringify is used as a good default,
265
+ * but can be overridden. */
266
+ stringify(/* value */) {},
267
+
207
268
  /* Update documents in a collection
208
269
  * and return the number of modified documents. */
209
270
  update(/* Coll, selector, modifier, options */) {},
@@ -212,6 +273,54 @@ const customProtocol = {
212
273
  setProtocol(customProtocol);
213
274
  ```
214
275
 
276
+ `observe` expected shape:
277
+
278
+ ```js
279
+ observe(Coll, selector = {}, callbacks = {}, options = {}) {
280
+ const { added, changed, removed } = callbacks;
281
+
282
+ // Your implementation subscribes reactively to selector/options changes.
283
+ // Callbacks should be invoked with:
284
+ // - added(id, fields)
285
+ // - changed(id, fields)
286
+ // - removed(id)
287
+ //
288
+ // Then return an object exposing a stop() method.
289
+ return {
290
+ stop() {
291
+ // Tear down underlying observer/resources.
292
+ },
293
+ };
294
+ }
295
+ ```
296
+
297
+ Notes:
298
+
299
+ - `observe` can be sync or async (returning a Promise of the stop-handle).
300
+ - `fields` should contain changed/added fields payload expected by your transport.
301
+ - `publish()` relies on this contract to keep nested observers in sync.
302
+
303
+ If your runtime has special callback context requirements, implement `bindEnvironment`.
304
+
305
+ ```js
306
+ const customProtocol = {
307
+ // ...
308
+ bindEnvironment(fn) {
309
+ if (typeof Meteor?.bindEnvironment === "function") {
310
+ return Meteor.bindEnvironment(fn);
311
+ }
312
+ return fn;
313
+ },
314
+ };
315
+ ```
316
+
317
+ If your runtime has no such requirement, simply omit `bindEnvironment`.
318
+
319
+ If you want to use the [`publish()`](#publishpublication-args-options) composite publication helper:
320
+
321
+ - `observe` must be defined on the protocol.
322
+ - `stringify` can be defined if the default EJSON implementation is not sufficient.
323
+
215
324
  </details>
216
325
 
217
326
  ## Bypassing `coll-fns`
@@ -413,7 +522,7 @@ join(Workers, {
413
522
 
414
523
  When joins are too complex to be defined with an array or object (although rare), a function can be used as the `on` property. Each parent document will be passed to this function, which should return a selector to use on the child collection.
415
524
 
416
- When using function-based joins, **a `fields` property should be added** to the join definition to declare which fields the parent document needs for the join to work:
525
+ When using function-based joins, **a `deps` property should be added** to the join definition to declare which parent fields are required for the join to work:
417
526
 
418
527
  ```js
419
528
  import { join } from "coll-fns";
@@ -436,7 +545,7 @@ join(Posts, {
436
545
  };
437
546
  },
438
547
  /* Parent fields needed in the join function */
439
- fields: {
548
+ deps: {
440
549
  _id: 1, // Optional. _id is implicit in any fetch.
441
550
  postedAt: 1,
442
551
  },
@@ -444,6 +553,8 @@ join(Posts, {
444
553
  });
445
554
  ```
446
555
 
556
+ `fields` remains accepted as a backward-compatible alias for `deps`.
557
+
447
558
  ### Recursive joins
448
559
 
449
560
  A collection can define joins on itself.
@@ -463,7 +574,7 @@ join(Users, {
463
574
 
464
575
  ### Join additional options
465
576
 
466
- Any additional properties defined on the join (other than `Coll`, `on`, `single`, `postFetch`) will be treated as options to pass to the nested documents `fetchList`. It usually includes:
577
+ Any additional properties defined on the join (other than `Coll`, `on`, `single`, `postFetch`, `deps` and legacy `fields`) will be treated as options to pass to the nested documents `fetchList`. It usually includes:
467
578
 
468
579
  - `limit`: Maximum joined documents count
469
580
  - `skip`: Documents to skip in the fetch
@@ -473,7 +584,7 @@ Any additional properties defined on the join (other than `Coll`, `on`, `single`
473
584
 
474
585
  Children documents might need to be modified (transformed, ordered, filtered...) after being fetched. The `postFetch: (childrenDocs, parentDoc) => childrenDocs` join definition property can be used to do so.
475
586
 
476
- The second argument of the function is the parent document. If some of its properties are needed, they should be declared in the `fields` property to ensure they are not missing from the requested fetched fields.
587
+ The second argument of the function is the parent document. If some of its properties are needed, they should be declared in the join `deps` property so they are guaranteed to be fetched on the parent.
477
588
 
478
589
  ```js
479
590
  import { join } from "coll-fns";
@@ -485,8 +596,8 @@ join(Resources, {
485
596
  Coll: Tasks,
486
597
  on: ["_id", "resourceId"],
487
598
 
488
- /* Ensure `tasksOrder` will be fetched */
489
- fields: { tasksOrder: 1 },
599
+ /* Ensure `tasksOrder` will be fetched on parent docs */
600
+ deps: { tasksOrder: 1 },
490
601
 
491
602
  /* Transform the joined tasks documents based on parent resource. */
492
603
  postFetch(tasks, resource) {
@@ -1027,12 +1138,14 @@ Each hook definition is an object with the following properties:
1027
1138
  * needed anyway to fetch their "after" versions). */
1028
1139
  before: true,
1029
1140
 
1030
- /* Optional. Synchronous predicate that prevents the hook from running if it
1031
- * returns a truthy value. Receives the same arguments as fn. */
1141
+ /* Optional. Predicate that prevents the hook from running if it
1142
+ * returns a truthy value. Can be sync or async.
1143
+ * Receives the same arguments as fn. */
1032
1144
  unless(doc) { return doc.isBot; },
1033
1145
 
1034
- /* Optional. Synchronous predicate that allows the hook to run only if it
1035
- * returns a truthy value. Receives the same arguments as fn. */
1146
+ /* Optional. Predicate that allows the hook to run only if it
1147
+ * returns a truthy value. Can be sync or async.
1148
+ * Receives the same arguments as fn. */
1036
1149
  when(doc) { return doc.status === "pending"; },
1037
1150
 
1038
1151
  /* Optional handler called if the hook function throws an error.
@@ -1483,6 +1596,340 @@ hook(Users, {
1483
1596
 
1484
1597
  </details>
1485
1598
 
1599
+ # Nested reactive publications
1600
+
1601
+ If `coll-fns` is used in a Meteor project, using publications is a way to create fully reactive applications. `publish` function helps to publish complex hierarchical data.
1602
+
1603
+ ## When to use `publish`
1604
+
1605
+ Use `publish()` when the publication tree is dynamic and cannot be represented as a simple static list of cursors.
1606
+
1607
+ **Important caveat (Meteor):** if a publication can return plain cursor(s) directly from `Meteor.publish`, prefer that approach. Native cursor-return publications are simpler and usually more optimized by Meteor internals than any userland helper.
1608
+
1609
+ `publish()` is intended for cases where you need one or more of:
1610
+
1611
+ - nested reactive children depending on parent documents
1612
+ - selector recomputation based on changed parent fields
1613
+ - observer reuse and invalidation logic you do not want to hand-roll repeatedly
1614
+
1615
+ ## `publish(publication, Coll, selector, options)`
1616
+
1617
+ Create a reactive publication tree (Meteor-style) with support for:
1618
+
1619
+ - explicit child observers (`{ Coll, on, ... }`)
1620
+ - join shorthand children (`{ join: "joinKey", ... }`)
1621
+ - implicit join children derived from parent requested join fields
1622
+
1623
+ `publish` internally uses protocol methods:
1624
+
1625
+ - `observe` to track cursor changes
1626
+ - `getName` to emit DDP collection names
1627
+ - `stringify` to build stable query reuse keys
1628
+
1629
+ As with normal joins, child `on` values can be:
1630
+
1631
+ - static selector objects
1632
+ - functions `(parent, ...ancestors) => selector`
1633
+ - join-array selectors: `[from, to, toSelector?]`
1634
+
1635
+ Refer to the [`join()`](#joincoll-joindefinitions) section for more details.
1636
+
1637
+ ```js
1638
+ import { Meteor } from "meteor/meteor";
1639
+ import { join, publish, setJoinPrefix } from "coll-fns";
1640
+ import {
1641
+ Posts,
1642
+ Users,
1643
+ Comments,
1644
+ Tags,
1645
+ FeatureFlags,
1646
+ PostStats,
1647
+ } from "/imports/api/collections";
1648
+
1649
+ setJoinPrefix("+");
1650
+
1651
+ join(Posts, {
1652
+ author: { Coll: Users, on: ["authorId", "_id"], single: true },
1653
+ comments: { Coll: Comments, on: ["_id", "postId"], sort: { createdAt: -1 } },
1654
+ stats: { Coll: PostStats, on: ["_id", "postId"], single: true },
1655
+ });
1656
+
1657
+ join(Comments, {
1658
+ author: { Coll: Users, on: ["authorId", "_id"], single: true },
1659
+ });
1660
+
1661
+ Meteor.publish("posts.tree", function postsTree() {
1662
+ return publish(
1663
+ this,
1664
+ Posts,
1665
+ { status: "published" },
1666
+ {
1667
+ fields: {
1668
+ title: 1,
1669
+ authorId: 1,
1670
+ tagIds: 1,
1671
+ editorId: 1,
1672
+ "+": {
1673
+ stats: 1,
1674
+ comments: {
1675
+ body: 1,
1676
+ createdAt: 1,
1677
+ "+": {
1678
+ author: { displayName: 1 },
1679
+ },
1680
+ },
1681
+ },
1682
+ },
1683
+ children: [
1684
+ /* Predefined join on Posts collection */
1685
+ {
1686
+ join: "author",
1687
+ fields: { displayName: 1, avatarUrl: 1 },
1688
+ },
1689
+
1690
+ /* Array selector */
1691
+ {
1692
+ Coll: Tags,
1693
+ on: [["tagIds"], "_id"],
1694
+ fields: { label: 1, color: 1 },
1695
+ deps: undefined, // Array selector children implicitely derive deps
1696
+ },
1697
+
1698
+ /* Function selector */
1699
+ {
1700
+ Coll: Users,
1701
+ on: (post) => ({ _id: post.editorId }),
1702
+ fields: { displayName: 1 },
1703
+ deps: ["editorId"],
1704
+ },
1705
+
1706
+ /* Object selector. Always returns the same data irrespective of parent. */
1707
+ {
1708
+ Coll: FeatureFlags,
1709
+ on: { scope: "posts_publication" },
1710
+ fields: { key: 1, enabled: 1 },
1711
+ deps: false,
1712
+ },
1713
+ ],
1714
+ }
1715
+ );
1716
+ });
1717
+ ```
1718
+
1719
+ `options.maxConcurrent` controls how many child observer creations can run at
1720
+ the same time (`10` by default).
1721
+
1722
+ Why it matters:
1723
+
1724
+ - each parent `added`/`changed` can trigger many child observer creations
1725
+ - unbounded concurrency can create CPU spikes and DB pressure
1726
+ - too little concurrency can slow initial publication warm-up
1727
+
1728
+ What it controls exactly:
1729
+
1730
+ - only child observer creation tasks are throttled
1731
+ - observer reuse still applies (duplicate query keys are collapsed)
1732
+ - invalidation logic still runs; the option just smooths creation bursts
1733
+
1734
+ Practical guidance:
1735
+
1736
+ - decrease it if your publication causes heavy DB load or event-loop stalls
1737
+ - increase it if your DB/runtime handles parallelism well and warm-up is slow
1738
+ - keep in mind this is per `publish()` call, not a single global cap
1739
+
1740
+ Example:
1741
+
1742
+ ```js
1743
+ Meteor.publish("posts.tree", function postsTree() {
1744
+ return publish(
1745
+ this,
1746
+ Posts,
1747
+ { status: "published" },
1748
+ {
1749
+ ...args,
1750
+ maxConcurrent: 5,
1751
+ }
1752
+ );
1753
+ });
1754
+ ```
1755
+
1756
+ ## Publication context (`this` in Meteor)
1757
+
1758
+ `publish()` is designed first for Meteor publications. In Meteor usage, the first
1759
+ argument should be the publication session/context (`this`) received in
1760
+ `Meteor.publish(name, function () { ... })`.
1761
+
1762
+ Minimal expected shape of `publication`:
1763
+
1764
+ - `ready()` (required)
1765
+ - `added(collectionName, id, fields)` (optional but normally provided by Meteor)
1766
+ - `changed(collectionName, id, fields)` (optional but normally provided by Meteor)
1767
+ - `removed(collectionName, id)` (optional but normally provided by Meteor)
1768
+ - `onStop(fn)` (optional, used for cleanup registration)
1769
+ - `error(err)` (optional, used as error sink)
1770
+
1771
+ Example:
1772
+
1773
+ ```js
1774
+ Meteor.publish("posts.tree", function () {
1775
+ // `this` is the publication context/session.
1776
+ return publish(this, Posts, { status: "published" });
1777
+ });
1778
+ ```
1779
+
1780
+ If `ready` is missing, `publish()` throws.
1781
+
1782
+ ## How child declarations work
1783
+
1784
+ `children` entries can be objects or falsy values (`false`, `null`, `undefined`).
1785
+ Falsy entries are ignored, which allows short-circuit declarations like
1786
+ `isEnabled && { ...childArgs }`.
1787
+
1788
+ Object entries can be defined with either:
1789
+
1790
+ - explicit child args:
1791
+ - `{ Coll, on, fields?, deps?, children?, ...cursorOptions }`
1792
+ - join shorthand:
1793
+ - `{ join: "joinKey", ...overrides }`
1794
+
1795
+ Additionally, join children can be derived implicitly from requested parent join fields.
1796
+
1797
+ Conflict rule:
1798
+
1799
+ - If the same join key is declared both as explicit child (`{ join: "..." }`) and in parent join fields (`fields["+"][joinKey]` or root join key without prefix), `publish()` throws and asks you to choose one style.
1800
+
1801
+ ## Ancestors chain
1802
+
1803
+ For function selectors and function deps, the helper passes:
1804
+
1805
+ - first argument: direct parent document
1806
+ - remaining arguments: full ancestors chain (grandparent, great-grandparent, ...)
1807
+
1808
+ This is useful when deep children must depend on context from higher levels.
1809
+
1810
+ ```js
1811
+ Meteor.publish("resources.tasks.actions", function () {
1812
+ return publish(
1813
+ this,
1814
+ Resources,
1815
+ { archived: false },
1816
+ {
1817
+ children: [
1818
+ {
1819
+ Coll: Tasks,
1820
+ on: (resource) => ({ resourceId: resource._id }),
1821
+ deps: ["_id"],
1822
+ children: [
1823
+ {
1824
+ Coll: Actions,
1825
+ on: (task, resource) => ({
1826
+ taskId: task._id,
1827
+ tenantId: resource.tenantId,
1828
+ }),
1829
+ deps(changedFields, task, resource) {
1830
+ // Re-run when task link changes or resource tenancy changes.
1831
+ if ("tenantId" in changedFields) return true;
1832
+ return ["_id", "tenantId"];
1833
+ },
1834
+ },
1835
+ ],
1836
+ },
1837
+ ],
1838
+ }
1839
+ );
1840
+ });
1841
+ ```
1842
+
1843
+ ## How `deps` works
1844
+
1845
+ `deps` controls when child observers are invalidated and recomputed after parent `changed` events.
1846
+
1847
+ Supported values:
1848
+
1849
+ - `true`: always invalidate
1850
+ - `false`: never invalidate
1851
+ - `"field"` or `["fieldA", "fieldB"]`: invalidate only when those keys are present in changed fields
1852
+ - `{ fieldA: 1, fieldB: true }`: object shorthand converted to watched keys (truthy top-level keys only)
1853
+ - function `(changedFields, parent, ...ancestors) => depsLike`: dynamic rule
1854
+ - `undefined`: special behavior
1855
+
1856
+ Special behavior when `deps` is `undefined`:
1857
+
1858
+ - static selector object child: treated as no invalidation (`[]`)
1859
+ - array/function selector child: treated as potentially dependent and will always invalidate conservatively
1860
+
1861
+ For array selectors, implicit deps are auto-derived from the `from` key.
1862
+
1863
+ `deps` matching is flat:
1864
+
1865
+ - matching is done against exact keys present in `changedFields`
1866
+ - no deep path traversal is performed by `publish()`
1867
+ - nested object deps only contribute top-level keys
1868
+
1869
+ ## Debugging
1870
+
1871
+ `publish()` supports lightweight lifecycle debugging with:
1872
+
1873
+ - `debug: true` to log all internal debug events
1874
+ - `debug: { EVENT_NAME: true, ... }` to log selected events only
1875
+
1876
+ Examples of event names include:
1877
+
1878
+ - `CREATED`, `REUSED`
1879
+ - `INVALIDATED`
1880
+ - `DOC_ADDED`, `DOC_CHANGED`, `DOC_REMOVED`
1881
+ - `CANCELLED`, `UNFOLLOWED`, `STOPPED`, `READY`
1882
+
1883
+ Debug scope:
1884
+
1885
+ - Root `debug` applies to the root observer.
1886
+ - Explicit children can override with their own `debug`.
1887
+ - Join-shorthand children (`{ join: "x", ... }`) can also provide `debug`.
1888
+ - Implicit join-derived children (from parent `fields`) inherit parent `debug`.
1889
+
1890
+ ## Built-in optimizations
1891
+
1892
+ `publish()` is designed to stay controlled even with nested reactive trees.
1893
+
1894
+ In practical terms, it aims to protect you from:
1895
+
1896
+ - creating the same observer repeatedly for equivalent child queries
1897
+ - runaway bursts of child observer creation
1898
+ - stale async creations being attached after data already changed
1899
+ - leaked child observers when parent links disappear
1900
+ - duplicate add/remove churn for documents shared by multiple branches
1901
+
1902
+ Why this matters:
1903
+
1904
+ - lower risk of memory growth from forgotten/stale observers
1905
+ - fewer unnecessary observers and DB watches
1906
+ - more predictable behavior during frequent parent changes
1907
+ - safer use of nested publications in real apps, not only toy examples
1908
+
1909
+ `maxConcurrent` is part of this safety model: it prevents uncontrolled parallel
1910
+ creation bursts and lets you tune throughput vs load.
1911
+
1912
+ ## Using `publish` outside Meteor
1913
+
1914
+ The helper can be used outside Meteor only if both layers are provided:
1915
+
1916
+ - protocol layer (`setProtocol`) supporting at least:
1917
+ - `observe`
1918
+ - `getName` (optional, default implementation provided)
1919
+ - `stringify` (optional, default implementation provided)
1920
+ - publication transport/context object implementing the callbacks [listed above](#publication-context-this-in-meteor)
1921
+ (`added/changed/removed/ready/onStop/error`)
1922
+
1923
+ Protocol methods handle database reactivity.
1924
+ `publication` handles how data changes are emitted to clients.
1925
+
1926
+ ## Current limitations
1927
+
1928
+ - `publish()` is a helper, not a replacement for simple cursor-return publications.
1929
+ - Deep/dynamic trees can still be expensive if selectors are broad and highly volatile.
1930
+ - If `deps` are too broad (or omitted for dynamic selectors), invalidations may be frequent.
1931
+ - For best results, keep parent selectors selective and declare precise `deps`.
1932
+
1486
1933
  # License
1487
1934
 
1488
1935
  MIT