coll-fns 1.4.3 → 1.6.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 +447 -7
- package/dist/coll-fns.cjs +1 -1
- package/dist/coll-fns.cjs.map +1 -1
- package/dist/coll-fns.mjs +1 -1
- package/dist/coll-fns.mjs.map +1 -1
- package/package.json +18 -10
- package/src/index.d.ts +72 -0
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,18 @@ 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 readiness](#publication-readiness)
|
|
61
|
+
- [Publication context (`this` in Meteor)](#publication-context-this-in-meteor)
|
|
62
|
+
- [How child declarations work](#how-child-declarations-work)
|
|
63
|
+
- [Ancestors chain](#ancestors-chain)
|
|
64
|
+
- [How `deps` works](#how-deps-works)
|
|
65
|
+
- [Debugging](#debugging)
|
|
66
|
+
- [Built-in optimizations](#built-in-optimizations)
|
|
67
|
+
- [Using `publish` outside Meteor](#using-publish-outside-meteor)
|
|
68
|
+
- [Current limitations](#current-limitations)
|
|
56
69
|
- [License](#license)
|
|
57
70
|
|
|
58
71
|
# Rationale
|
|
@@ -122,7 +135,7 @@ And since it uses a protocol, it can be used with the **native MongoDB driver**
|
|
|
122
135
|
|
|
123
136
|
</details>
|
|
124
137
|
|
|
125
|
-
<details
|
|
138
|
+
<details>
|
|
126
139
|
<summary><strong>Functional API</strong></summary>
|
|
127
140
|
|
|
128
141
|
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 +152,26 @@ For Meteor developers, it also means being able to enhance the `Meteor.users` co
|
|
|
139
152
|
|
|
140
153
|
</details>
|
|
141
154
|
|
|
155
|
+
<details style="margin-bottom: 1rem">
|
|
156
|
+
<summary><strong>Nested reactive publications</strong></summary>
|
|
157
|
+
|
|
158
|
+
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.
|
|
159
|
+
|
|
160
|
+
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!
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
Some existing community libraries that promise to do so either:
|
|
165
|
+
|
|
166
|
+
- don't actually work (might understand `added` and `removed` callbacks, but won't react to updates)
|
|
167
|
+
- are too simplistic (cause a lot of duplicated observers)
|
|
168
|
+
- come with a much more complex data layer
|
|
169
|
+
- add somewhat unpredictable reactivity on the server.
|
|
170
|
+
|
|
171
|
+
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. 💪
|
|
172
|
+
|
|
173
|
+
</details>
|
|
174
|
+
|
|
142
175
|
# Installation and configuration
|
|
143
176
|
|
|
144
177
|
**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.
|
|
@@ -220,10 +253,19 @@ const customProtocol = {
|
|
|
220
253
|
* and return the inserted _id. */
|
|
221
254
|
insert(/* Coll, doc, options */) {},
|
|
222
255
|
|
|
256
|
+
/* Observe document changes.
|
|
257
|
+
* Must return a handle with a `stop()` method. */
|
|
258
|
+
observe(/* Coll, selector = {}, callbacks = {}, options = {} */) {},
|
|
259
|
+
|
|
223
260
|
/* Remove documents in a collection
|
|
224
261
|
* and return the number of removed documents. */
|
|
225
262
|
remove(/* Coll, selector, options */) {},
|
|
226
263
|
|
|
264
|
+
/* Stable stringify function used for internal query keys.
|
|
265
|
+
* EJSON canonical stringify is used as a good default,
|
|
266
|
+
* but can be overridden. */
|
|
267
|
+
stringify(/* value */) {},
|
|
268
|
+
|
|
227
269
|
/* Update documents in a collection
|
|
228
270
|
* and return the number of modified documents. */
|
|
229
271
|
update(/* Coll, selector, modifier, options */) {},
|
|
@@ -232,6 +274,33 @@ const customProtocol = {
|
|
|
232
274
|
setProtocol(customProtocol);
|
|
233
275
|
```
|
|
234
276
|
|
|
277
|
+
`observe` expected shape:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
observe(Coll, selector = {}, callbacks = {}, options = {}) {
|
|
281
|
+
const { added, changed, removed } = callbacks;
|
|
282
|
+
|
|
283
|
+
// Your implementation subscribes reactively to selector/options changes.
|
|
284
|
+
// Callbacks should be invoked with:
|
|
285
|
+
// - added(id, fields)
|
|
286
|
+
// - changed(id, fields)
|
|
287
|
+
// - removed(id)
|
|
288
|
+
//
|
|
289
|
+
// Then return an object exposing a stop() method.
|
|
290
|
+
return {
|
|
291
|
+
stop() {
|
|
292
|
+
// Tear down underlying observer/resources.
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Notes:
|
|
299
|
+
|
|
300
|
+
- `observe` can be sync or async (returning a Promise of the stop-handle).
|
|
301
|
+
- `fields` should contain changed/added fields payload expected by your transport.
|
|
302
|
+
- `publish()` relies on this contract to keep nested observers in sync.
|
|
303
|
+
|
|
235
304
|
If your runtime has special callback context requirements, implement `bindEnvironment`.
|
|
236
305
|
|
|
237
306
|
```js
|
|
@@ -248,6 +317,11 @@ const customProtocol = {
|
|
|
248
317
|
|
|
249
318
|
If your runtime has no such requirement, simply omit `bindEnvironment`.
|
|
250
319
|
|
|
320
|
+
If you want to use the [`publish()`](#publishpublication-args-options) composite publication helper:
|
|
321
|
+
|
|
322
|
+
- `observe` must be defined on the protocol.
|
|
323
|
+
- `stringify` can be defined if the default EJSON implementation is not sufficient.
|
|
324
|
+
|
|
251
325
|
</details>
|
|
252
326
|
|
|
253
327
|
## Bypassing `coll-fns`
|
|
@@ -449,7 +523,7 @@ join(Workers, {
|
|
|
449
523
|
|
|
450
524
|
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.
|
|
451
525
|
|
|
452
|
-
When using function-based joins, **a `
|
|
526
|
+
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:
|
|
453
527
|
|
|
454
528
|
```js
|
|
455
529
|
import { join } from "coll-fns";
|
|
@@ -472,7 +546,7 @@ join(Posts, {
|
|
|
472
546
|
};
|
|
473
547
|
},
|
|
474
548
|
/* Parent fields needed in the join function */
|
|
475
|
-
|
|
549
|
+
deps: {
|
|
476
550
|
_id: 1, // Optional. _id is implicit in any fetch.
|
|
477
551
|
postedAt: 1,
|
|
478
552
|
},
|
|
@@ -480,6 +554,8 @@ join(Posts, {
|
|
|
480
554
|
});
|
|
481
555
|
```
|
|
482
556
|
|
|
557
|
+
`fields` remains accepted as a backward-compatible alias for `deps`.
|
|
558
|
+
|
|
483
559
|
### Recursive joins
|
|
484
560
|
|
|
485
561
|
A collection can define joins on itself.
|
|
@@ -499,7 +575,7 @@ join(Users, {
|
|
|
499
575
|
|
|
500
576
|
### Join additional options
|
|
501
577
|
|
|
502
|
-
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:
|
|
578
|
+
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:
|
|
503
579
|
|
|
504
580
|
- `limit`: Maximum joined documents count
|
|
505
581
|
- `skip`: Documents to skip in the fetch
|
|
@@ -509,7 +585,7 @@ Any additional properties defined on the join (other than `Coll`, `on`, `single`
|
|
|
509
585
|
|
|
510
586
|
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.
|
|
511
587
|
|
|
512
|
-
The second argument of the function is the parent document. If some of its properties are needed, they should be declared in the `
|
|
588
|
+
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.
|
|
513
589
|
|
|
514
590
|
```js
|
|
515
591
|
import { join } from "coll-fns";
|
|
@@ -521,8 +597,8 @@ join(Resources, {
|
|
|
521
597
|
Coll: Tasks,
|
|
522
598
|
on: ["_id", "resourceId"],
|
|
523
599
|
|
|
524
|
-
/* Ensure `tasksOrder` will be fetched */
|
|
525
|
-
|
|
600
|
+
/* Ensure `tasksOrder` will be fetched on parent docs */
|
|
601
|
+
deps: { tasksOrder: 1 },
|
|
526
602
|
|
|
527
603
|
/* Transform the joined tasks documents based on parent resource. */
|
|
528
604
|
postFetch(tasks, resource) {
|
|
@@ -1521,6 +1597,370 @@ hook(Users, {
|
|
|
1521
1597
|
|
|
1522
1598
|
</details>
|
|
1523
1599
|
|
|
1600
|
+
# Nested reactive publications
|
|
1601
|
+
|
|
1602
|
+
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.
|
|
1603
|
+
|
|
1604
|
+
## When to use `publish`
|
|
1605
|
+
|
|
1606
|
+
Use `publish()` when the publication tree is dynamic and cannot be represented as a simple static list of cursors.
|
|
1607
|
+
|
|
1608
|
+
**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.
|
|
1609
|
+
|
|
1610
|
+
`publish()` is intended for cases where you need one or more of:
|
|
1611
|
+
|
|
1612
|
+
- nested reactive children depending on parent documents
|
|
1613
|
+
- selector recomputation based on changed parent fields
|
|
1614
|
+
- observer reuse and invalidation logic you do not want to hand-roll repeatedly
|
|
1615
|
+
|
|
1616
|
+
## `publish(publication, Coll, selector, options)`
|
|
1617
|
+
|
|
1618
|
+
Create a reactive publication tree (Meteor-style) with support for:
|
|
1619
|
+
|
|
1620
|
+
- explicit child observers (`{ Coll, on, ... }`)
|
|
1621
|
+
- join shorthand children (`{ join: "joinKey", ... }`)
|
|
1622
|
+
- implicit join children derived from parent requested join fields
|
|
1623
|
+
|
|
1624
|
+
`publish` internally uses protocol methods:
|
|
1625
|
+
|
|
1626
|
+
- `observe` to track cursor changes
|
|
1627
|
+
- `getName` to emit DDP collection names
|
|
1628
|
+
- `stringify` to build stable query reuse keys
|
|
1629
|
+
|
|
1630
|
+
As with normal joins, child `on` values can be:
|
|
1631
|
+
|
|
1632
|
+
- static selector objects
|
|
1633
|
+
- functions `(parent, ...ancestors) => selector`
|
|
1634
|
+
- join-array selectors: `[from, to, toSelector?]`
|
|
1635
|
+
|
|
1636
|
+
Refer to the [`join()`](#joincoll-joindefinitions) section for more details.
|
|
1637
|
+
|
|
1638
|
+
```js
|
|
1639
|
+
import { Meteor } from "meteor/meteor";
|
|
1640
|
+
import { join, publish, setJoinPrefix } from "coll-fns";
|
|
1641
|
+
import {
|
|
1642
|
+
Posts,
|
|
1643
|
+
Users,
|
|
1644
|
+
Comments,
|
|
1645
|
+
Tags,
|
|
1646
|
+
FeatureFlags,
|
|
1647
|
+
PostStats,
|
|
1648
|
+
} from "/imports/api/collections";
|
|
1649
|
+
|
|
1650
|
+
setJoinPrefix("+");
|
|
1651
|
+
|
|
1652
|
+
join(Posts, {
|
|
1653
|
+
author: { Coll: Users, on: ["authorId", "_id"], single: true },
|
|
1654
|
+
comments: { Coll: Comments, on: ["_id", "postId"], sort: { createdAt: -1 } },
|
|
1655
|
+
stats: { Coll: PostStats, on: ["_id", "postId"], single: true },
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
join(Comments, {
|
|
1659
|
+
author: { Coll: Users, on: ["authorId", "_id"], single: true },
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
Meteor.publish("posts.tree", function postsTree() {
|
|
1663
|
+
return publish(
|
|
1664
|
+
this,
|
|
1665
|
+
Posts,
|
|
1666
|
+
{ status: "published" },
|
|
1667
|
+
{
|
|
1668
|
+
fields: {
|
|
1669
|
+
title: 1,
|
|
1670
|
+
authorId: 1,
|
|
1671
|
+
tagIds: 1,
|
|
1672
|
+
editorId: 1,
|
|
1673
|
+
"+": {
|
|
1674
|
+
stats: 1,
|
|
1675
|
+
comments: {
|
|
1676
|
+
body: 1,
|
|
1677
|
+
createdAt: 1,
|
|
1678
|
+
"+": {
|
|
1679
|
+
author: { displayName: 1 },
|
|
1680
|
+
},
|
|
1681
|
+
},
|
|
1682
|
+
},
|
|
1683
|
+
},
|
|
1684
|
+
children: [
|
|
1685
|
+
/* Predefined join on Posts collection */
|
|
1686
|
+
{
|
|
1687
|
+
join: "author",
|
|
1688
|
+
fields: { displayName: 1, avatarUrl: 1 },
|
|
1689
|
+
},
|
|
1690
|
+
|
|
1691
|
+
/* Array selector */
|
|
1692
|
+
{
|
|
1693
|
+
Coll: Tags,
|
|
1694
|
+
on: [["tagIds"], "_id"],
|
|
1695
|
+
fields: { label: 1, color: 1 },
|
|
1696
|
+
deps: undefined, // Array selector children implicitely derive deps
|
|
1697
|
+
},
|
|
1698
|
+
|
|
1699
|
+
/* Function selector */
|
|
1700
|
+
{
|
|
1701
|
+
Coll: Users,
|
|
1702
|
+
on: (post) => ({ _id: post.editorId }),
|
|
1703
|
+
fields: { displayName: 1 },
|
|
1704
|
+
deps: ["editorId"],
|
|
1705
|
+
},
|
|
1706
|
+
|
|
1707
|
+
/* Object selector. Always returns the same data irrespective of parent. */
|
|
1708
|
+
{
|
|
1709
|
+
Coll: FeatureFlags,
|
|
1710
|
+
on: { scope: "posts_publication" },
|
|
1711
|
+
fields: { key: 1, enabled: 1 },
|
|
1712
|
+
deps: false,
|
|
1713
|
+
},
|
|
1714
|
+
],
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
});
|
|
1718
|
+
```
|
|
1719
|
+
|
|
1720
|
+
## Publication readiness and concurrency
|
|
1721
|
+
|
|
1722
|
+
Readiness is controlled per child with `awaited` (default: `true`):
|
|
1723
|
+
|
|
1724
|
+
- `awaited: true`: wait for this child subtree before calling `ready()`
|
|
1725
|
+
- `awaited: false`: do not wait for this child subtree; it loads/reacts in background
|
|
1726
|
+
|
|
1727
|
+
Use `awaited: true` for data the screen must have immediately, and `awaited: false` for optional or heavy branches.
|
|
1728
|
+
If not specified, a child inherits `awaited` from its parent.
|
|
1729
|
+
|
|
1730
|
+
`options.maxConcurrent` controls how many child observer creations can run at
|
|
1731
|
+
the same time (`10` by default). This can only be defined at the publication root, not on children.
|
|
1732
|
+
|
|
1733
|
+
Why it matters:
|
|
1734
|
+
|
|
1735
|
+
- each parent `added`/`changed` can trigger many child observer creations
|
|
1736
|
+
- unbounded concurrency can create CPU spikes and DB pressure
|
|
1737
|
+
- too little concurrency can slow initial publication warm-up
|
|
1738
|
+
|
|
1739
|
+
What it controls exactly:
|
|
1740
|
+
|
|
1741
|
+
- only child observer creation tasks are throttled
|
|
1742
|
+
- observer reuse still applies (duplicate query keys are collapsed)
|
|
1743
|
+
- invalidation logic still runs; the option just smooths creation bursts
|
|
1744
|
+
|
|
1745
|
+
Practical guidance:
|
|
1746
|
+
|
|
1747
|
+
- decrease it if your publication causes heavy DB load or event-loop stalls
|
|
1748
|
+
- increase it if your DB/runtime handles parallelism well and warm-up is slow
|
|
1749
|
+
- keep in mind this is per `publish()` call, not a single global cap
|
|
1750
|
+
|
|
1751
|
+
Example:
|
|
1752
|
+
|
|
1753
|
+
```js
|
|
1754
|
+
Meteor.publish("posts.tree", function postsTree() {
|
|
1755
|
+
return publish(
|
|
1756
|
+
this,
|
|
1757
|
+
Posts,
|
|
1758
|
+
{ status: "published" },
|
|
1759
|
+
{
|
|
1760
|
+
maxConcurrent: 5,
|
|
1761
|
+
children: [
|
|
1762
|
+
{ Coll: Users, on: ["authorId", "_id"], awaited: true },
|
|
1763
|
+
{
|
|
1764
|
+
Coll: FeatureFlags,
|
|
1765
|
+
on: { scope: "posts_publication" },
|
|
1766
|
+
awaited: false,
|
|
1767
|
+
},
|
|
1768
|
+
],
|
|
1769
|
+
}
|
|
1770
|
+
);
|
|
1771
|
+
});
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
## Publication context (`this` in Meteor)
|
|
1775
|
+
|
|
1776
|
+
`publish()` is designed first for Meteor publications. In Meteor usage, the first
|
|
1777
|
+
argument should be the publication session/context (`this`) received in
|
|
1778
|
+
`Meteor.publish(name, function () { ... })`.
|
|
1779
|
+
|
|
1780
|
+
Minimal expected shape of `publication`:
|
|
1781
|
+
|
|
1782
|
+
- `ready()` (required)
|
|
1783
|
+
- `added(collectionName, id, fields)` (optional but normally provided by Meteor)
|
|
1784
|
+
- `changed(collectionName, id, fields)` (optional but normally provided by Meteor)
|
|
1785
|
+
- `removed(collectionName, id)` (optional but normally provided by Meteor)
|
|
1786
|
+
- `onStop(fn)` (optional, used for cleanup registration)
|
|
1787
|
+
- `error(err)` (optional, used as error sink)
|
|
1788
|
+
|
|
1789
|
+
Example:
|
|
1790
|
+
|
|
1791
|
+
```js
|
|
1792
|
+
Meteor.publish("posts.tree", function () {
|
|
1793
|
+
// `this` is the publication context/session.
|
|
1794
|
+
return publish(this, Posts, { status: "published" });
|
|
1795
|
+
});
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
If `ready` is missing, `publish()` throws.
|
|
1799
|
+
|
|
1800
|
+
## How child declarations work
|
|
1801
|
+
|
|
1802
|
+
`children` entries can be objects or falsy values (`false`, `null`, `undefined`).
|
|
1803
|
+
Falsy entries are ignored, which allows short-circuit declarations like
|
|
1804
|
+
`isEnabled && { ...childArgs }`.
|
|
1805
|
+
|
|
1806
|
+
Object entries can be defined with either:
|
|
1807
|
+
|
|
1808
|
+
- explicit child args:
|
|
1809
|
+
- `{ Coll, on, fields?, deps?, awaited?, children?, ...cursorOptions }`
|
|
1810
|
+
- join shorthand:
|
|
1811
|
+
- `{ join: "joinKey", ...overrides }`
|
|
1812
|
+
|
|
1813
|
+
Additionally, join children can be derived implicitly from requested parent join fields.
|
|
1814
|
+
|
|
1815
|
+
Conflict rule:
|
|
1816
|
+
|
|
1817
|
+
- 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.
|
|
1818
|
+
|
|
1819
|
+
## Ancestors chain
|
|
1820
|
+
|
|
1821
|
+
For function selectors and function deps, the helper passes:
|
|
1822
|
+
|
|
1823
|
+
- first argument: direct parent document
|
|
1824
|
+
- remaining arguments: full ancestors chain (grandparent, great-grandparent, ...)
|
|
1825
|
+
|
|
1826
|
+
This is useful when deep children must depend on context from higher levels.
|
|
1827
|
+
|
|
1828
|
+
```js
|
|
1829
|
+
Meteor.publish("resources.tasks.actions", function () {
|
|
1830
|
+
return publish(
|
|
1831
|
+
this,
|
|
1832
|
+
Resources,
|
|
1833
|
+
{ archived: false },
|
|
1834
|
+
{
|
|
1835
|
+
children: [
|
|
1836
|
+
{
|
|
1837
|
+
Coll: Tasks,
|
|
1838
|
+
on: (resource) => ({ resourceId: resource._id }),
|
|
1839
|
+
deps: ["_id"],
|
|
1840
|
+
children: [
|
|
1841
|
+
{
|
|
1842
|
+
Coll: Actions,
|
|
1843
|
+
on: (task, resource) => ({
|
|
1844
|
+
taskId: task._id,
|
|
1845
|
+
tenantId: resource.tenantId,
|
|
1846
|
+
}),
|
|
1847
|
+
deps(changedFields, task, resource) {
|
|
1848
|
+
// Re-run when task link changes or resource tenancy changes.
|
|
1849
|
+
if ("tenantId" in changedFields) return true;
|
|
1850
|
+
return ["_id", "tenantId"];
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
],
|
|
1854
|
+
},
|
|
1855
|
+
],
|
|
1856
|
+
}
|
|
1857
|
+
);
|
|
1858
|
+
});
|
|
1859
|
+
```
|
|
1860
|
+
|
|
1861
|
+
## How `deps` works
|
|
1862
|
+
|
|
1863
|
+
`deps` controls when child observers are invalidated and recomputed after parent `changed` events.
|
|
1864
|
+
|
|
1865
|
+
Supported values:
|
|
1866
|
+
|
|
1867
|
+
- `true`: always invalidate
|
|
1868
|
+
- `false`: never invalidate
|
|
1869
|
+
- `"field"` or `["fieldA", "fieldB"]`: invalidate only when those keys are present in changed fields
|
|
1870
|
+
- `{ fieldA: 1, fieldB: true }`: object shorthand converted to watched keys (truthy top-level keys only)
|
|
1871
|
+
- function `(changedFields, parent, ...ancestors) => depsLike`: dynamic rule
|
|
1872
|
+
- `undefined`: special behavior
|
|
1873
|
+
|
|
1874
|
+
Special behavior when `deps` is `undefined`:
|
|
1875
|
+
|
|
1876
|
+
- static selector object child: treated as no invalidation (`[]`)
|
|
1877
|
+
- array/function selector child: treated as potentially dependent and will always invalidate conservatively
|
|
1878
|
+
|
|
1879
|
+
For array selectors, implicit deps are auto-derived from the `from` key.
|
|
1880
|
+
|
|
1881
|
+
`deps` matching is flat:
|
|
1882
|
+
|
|
1883
|
+
- matching is done against exact keys present in `changedFields`
|
|
1884
|
+
- no deep path traversal is performed by `publish()`
|
|
1885
|
+
- nested object deps only contribute top-level keys
|
|
1886
|
+
|
|
1887
|
+
## Debugging
|
|
1888
|
+
|
|
1889
|
+
`publish()` supports lightweight lifecycle debugging with:
|
|
1890
|
+
|
|
1891
|
+
- `debug: true` to log all internal debug events
|
|
1892
|
+
- `debug: ["EVENT_NAME", ...]` to log selected events only
|
|
1893
|
+
- `debug: { EVENT_NAME: true, ... }` to log selected events only
|
|
1894
|
+
|
|
1895
|
+
Observer events:
|
|
1896
|
+
|
|
1897
|
+
- `CREATED`: a new observer was successfully created and registered.
|
|
1898
|
+
- `BYPASSED`: observer creation was skipped because selector resolved to a void selector.
|
|
1899
|
+
- `REUSED`: an existing observer for the same query key was reused.
|
|
1900
|
+
- `INVALIDATED`: child observer graph for a parent document was recomputed after relevant parent changes.
|
|
1901
|
+
- `UNFOLLOWED`: one follower link to a sub-observer was removed.
|
|
1902
|
+
- `CANCELLED`: an observer was cancelled and its local cleanup started.
|
|
1903
|
+
|
|
1904
|
+
Observer documents events:
|
|
1905
|
+
|
|
1906
|
+
- `DOC_ADDED`: a document was published through `added`.
|
|
1907
|
+
- `DOC_CHANGED`: a published document emitted a `changed` update.
|
|
1908
|
+
- `DOC_REMOVED`: a document was removed from publication (observer count for that document dropped to zero).
|
|
1909
|
+
|
|
1910
|
+
Publication events (available on root only):
|
|
1911
|
+
|
|
1912
|
+
- `READY`: emitted once when `publish()` calls `publication.ready()`.
|
|
1913
|
+
- `STOPPED`: emitted once when the publication stop handler runs.
|
|
1914
|
+
|
|
1915
|
+
Debug scope:
|
|
1916
|
+
|
|
1917
|
+
- Root `debug` applies to the root observer and root publication locations (`READY`, `STOPPED`).
|
|
1918
|
+
- Each child defines its own `debug` argument.
|
|
1919
|
+
- Implicit join-derived children (from parent `fields`) inherit parent `debug`.
|
|
1920
|
+
|
|
1921
|
+
## Built-in optimizations
|
|
1922
|
+
|
|
1923
|
+
`publish()` is designed to stay controlled even with nested reactive trees.
|
|
1924
|
+
|
|
1925
|
+
In practical terms, it aims to protect you from:
|
|
1926
|
+
|
|
1927
|
+
- creating the same observer repeatedly for equivalent child queries
|
|
1928
|
+
- runaway bursts of child observer creation
|
|
1929
|
+
- stale async creations being attached after data already changed
|
|
1930
|
+
- leaked child observers when parent links disappear
|
|
1931
|
+
- duplicate add/remove churn for documents shared by multiple branches
|
|
1932
|
+
|
|
1933
|
+
Why this matters:
|
|
1934
|
+
|
|
1935
|
+
- lower risk of memory growth from forgotten/stale observers
|
|
1936
|
+
- fewer unnecessary observers and DB watches
|
|
1937
|
+
- more predictable behavior during frequent parent changes
|
|
1938
|
+
- safer use of nested publications in real apps, not only toy examples
|
|
1939
|
+
|
|
1940
|
+
`maxConcurrent` is part of this safety model: it prevents uncontrolled parallel
|
|
1941
|
+
creation bursts and lets you tune throughput vs load.
|
|
1942
|
+
|
|
1943
|
+
## Using `publish` outside Meteor
|
|
1944
|
+
|
|
1945
|
+
The helper can be used outside Meteor only if both layers are provided:
|
|
1946
|
+
|
|
1947
|
+
- protocol layer (`setProtocol`) supporting at least:
|
|
1948
|
+
- `observe`
|
|
1949
|
+
- `getName` (optional, default implementation provided)
|
|
1950
|
+
- `stringify` (optional, default implementation provided)
|
|
1951
|
+
- publication transport/context object implementing the callbacks [listed above](#publication-context-this-in-meteor)
|
|
1952
|
+
(`added/changed/removed/ready/onStop/error`)
|
|
1953
|
+
|
|
1954
|
+
Protocol methods handle database reactivity.
|
|
1955
|
+
`publication` handles how data changes are emitted to clients.
|
|
1956
|
+
|
|
1957
|
+
## Current limitations
|
|
1958
|
+
|
|
1959
|
+
- `publish()` is a helper, not a replacement for simple cursor-return publications.
|
|
1960
|
+
- Deep/dynamic trees can still be expensive if selectors are broad and highly volatile.
|
|
1961
|
+
- If `deps` are too broad (or omitted for dynamic selectors), invalidations may be frequent.
|
|
1962
|
+
- For best results, keep parent selectors selective and declare precise `deps`.
|
|
1963
|
+
|
|
1524
1964
|
# License
|
|
1525
1965
|
|
|
1526
1966
|
MIT
|