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 +458 -11
- 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 +21 -14
- package/src/index.d.ts +76 -2
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
|
|
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 `
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
1031
|
-
* returns a truthy value.
|
|
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.
|
|
1035
|
-
* returns a truthy value.
|
|
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
|