nachocodoner:reactive-publish

v1.1.0-beta.0Published 2 weeks ago

reactive-publish

reactive-publish is a Meteor package that adds reactive publishing with async support. It's based on peerlibrary:reactive-publish and peerlibrary:subscription-data, fully overhauled for compatibility with Meteor 3 and its fiber-free environment.

  • 🔄 Reactively publish data with related field changes across collections

  • 📊 Reactively publish derived data publishing without restarting subscriptions

  • ⚙️ Supports autorun in publication functions for realtime updates

  • 🧵 Integrates AsyncTracker/ReactiveVarAsync for *async-safe reactivity (server only)

  • 🚀 Optimized with unique cursors per computation to avoid redundant re-instantiation

🔥 Learn about the motivation for reviving this package for Meteor 3.

🗺️ Explore the roadmap for future updates and support.

Installation

meteor add nachocodoner:reactive-publish@1.0.0-rc.1

Usage

Reactive Composed Data Publish

Basic

1Meteor.publish('subscribed-posts', function () {
2  this.autorun(async () => {
3    const user = await User.findOneAsync(this.userId, {
4      fields: { subscribedPosts: 1 },
5    });
6
7    return Posts.find({ _id: { $in: user?.subscribedPosts || [] } });
8  });
9});

In the example above, you publish the user’s subscribed posts. When the User’s subscribedPosts field changes, autorun reruns and publishes the updated posts. Any queries with related data work the same way. You can also publish an array of cursors and use the same logic as in a normal publication body.

Since most use cases involve a single autorun block, you can use Meteor.publishReactive for cleaner syntax:

1Meteor.publishReactive('subscribed-posts', async function () {
2  const user = await User.findOneAsync(this.userId, {
3    fields: { subscribedPosts: 1 },
4  });
5
6  return Posts.find({ _id: { $in: user?.subscribedPosts || [] } });
7});

Time-based queries

1import { ReactiveVarAsync } from 'meteor/nachocodoner:reactive-publish';
2
3const currentTime = new ReactiveVarAsync(Date.now());
4
5Meteor.setInterval(() => {
6  currentTime.set(Date.now());
7}, 1000); // ms
8
9Meteor.publish('recent-posts', function () {
10  this.autorun(() => {
11    return Posts.find({
12      timestamp: {
13        $exists: true,
14        $gte: currentTime.get() - 60 * 1000,
15      },
16    }, {
17      sort: { timestamp: 1 },
18    });
19  });
20});

Multiple autoruns

1Meteor.publish('users-posts-and-addresses', function (userId) {
2  this.autorun(async () => {
3    const user = await Users.findOneAsync(userId, {
4      fields: { posts: 1 },
5    });
6    return Posts.find({ _id: { $in: user?.posts || [] } });
7  });
8
9  this.autorun(async () => {
10    const user = await Users.findOneAsync(userId, {
11      fields: { addresses: 1 },
12    });
13    return Addresses.find({ _id: { $in: user?.addresses || [] } });
14  });
15});

Reactive Derived Data Publish

These examples show how to publish derived data reactively, allowing clients to receive updates without restarting subscriptions.

Example 1: Infinite scrolling and counts

This example shows how to publish a collection with a reactive total count and allow the client to increase the limit of published items without restarting the subscription.

Server:

1import { Meteor } from 'meteor/meteor';
2import { check } from 'meteor/check';
3import { Mongo } from 'meteor/mongo';
4
5export const Posts = new Mongo.Collection('posts');
6
7Meteor.publish('posts.infiniteScroll', function () {
8  // Reactive total count
9  this.autorun(async () => {
10    await this.setData('countAll', await Posts.find().countAsync());
11  });
12
13  // Reactive window of posts, adjustable by client
14  this.autorun(async () => {
15    const limit = Number(await this.data('limit')) || 10;
16    check(limit, Number);
17    return Posts.find({}, { limit, sort: { createdAt: -1 } });
18  });
19});

Client (Tracker):

1import { Meteor } from 'meteor/meteor';
2import { Tracker } from 'meteor/tracker';
3import { Posts } from '/imports/api/posts.js';
4
5const sub = Meteor.subscribe('posts.infiniteScroll');
6
7// Reactive total count
8Tracker.autorun(async () => {
9  console.log('Total posts:', await sub.data('countAll'));
10});
11
12// Adjust published window without restarting
13sub.setData('limit', 20);

Example 2: External reactive source

This example shows how to publish data reactively from an external source. Here it's simulated with an interval toggling a user's subscription tier, but in practice it could be an API, another database, or any third-party service.

Server:

1import { Meteor } from 'meteor/meteor';
2
3Meteor.publish('user.subscriptionTier', function (userId) {
4  const pub = this;
5  let tier = 'free';
6
7  // Simulate external updates (It could be an API, database, or service)
8  const handle = setInterval(async () => {
9    tier = (tier === 'free') ? 'pro' : 'free';
10    await pub.setData('tier', tier);
11  }, 5000);
12
13  pub.onStop(() => clearInterval(handle));
14});

Client (React + useTracker):

1import React from 'react';
2import { Meteor } from 'meteor/meteor';
3import { useTracker } from 'meteor/react-meteor-data';
4
5export default function SubscriptionInfo({ userId }) {
6  const sub = Meteor.subscribe('user.subscriptionTier', userId);
7  const tier = useTracker(() => sub.data('tier'));
8
9  return <p>Subscription tier: {tier || 'loading...'}</p>;
10}

Example 3: Derived stats via aggregation

This example shows how to publish derived data from a Mongo aggregation.

It uses autorun to recalculate when the underlying collections change, and setData to push aggregated stats to the client.

Server:

1import { Meteor } from 'meteor/meteor';
2import { Mongo } from 'meteor/mongo';
3
4export const Posts = new Mongo.Collection('posts');
5export const Likes = new Mongo.Collection('likes');
6
7Meteor.publish('stats.authorActivity', function () {
8  const pub = this;
9
10  pub.autorun(async () => {
11    // Reactive deps: touching cursors ensures recalculation on changes
12    await Posts.find({}, { fields: { authorId: 1 } }).countAsync();
13    await Likes.find({}, { fields: { postId: 1 } }).countAsync();
14
15    const pipeline = [
16      { $lookup: { from: 'likes', localField: '_id', foreignField: 'postId', as: 'likes' } },
17      { $group: {
18          _id: '$authorId',
19          postsCount: { $sum: 1 },
20          likesCount: { $sum: { $size: '$likes' } },
21        }
22      },
23      { $project: { _id: 0, authorId: '$_id', postsCount: 1, likesCount: 1 } },
24    ];
25
26    const stats = await Posts.rawCollection().aggregate(pipeline).toArray();
27    await pub.setData('authorStats', stats);
28    
29    pub.ready();
30  });
31});

Client (React + useTracker):

1import React from 'react';
2import { Meteor } from 'meteor/meteor';
3import { useTracker } from 'meteor/react-meteor-data';
4
5export default function AuthorStats() {
6  const sub = Meteor.subscribe('stats.authorActivity');
7  const stats = useTracker(() => sub.data('authorStats') || []);
8
9  return (
10    <ul>
11      {stats.map(({ authorId, postsCount, likesCount }) => (
12        <li key={authorId}>
13          {`${authorId}: ${postsCount} posts · ${likesCount} likes`}
14        </li>
15      ))}
16    </ul>
17  );
18}

TypeScript Support

This package includes TypeScript definitions for all its APIs. To enable type checking:

  1. Make sure your app has zodern:types installed:
meteor add zodern:types
  1. Import the package using the Meteor import syntax:
1import { AsyncTracker, ReactiveVarAsync } from 'meteor/nachocodoner:reactive-publish';

This package extends Meteor's core types to add reactive publishing capabilities. The type definitions include:

  • Extensions to Meteor.publish context with the autorun, data and setData methods
  • The Meteor.publishReactive function
  • AsyncTracker and ReactiveVarAsync with full generic type support

Roadmap

  • Stability

    • Ensure core changes in this package don't affect Meteor core tests
    • Release betas and RCs, with a feedback period for early adopters
  • Expansion

    • Support for AsyncTracker and ReactiveVarAsync on the client
    • Support for publishing derived data reactively
  • Performance

Acknowledgments

This package builds on over a decade of work by PeerLibrary during the legacy Meteor era. Big thanks to everyone involved over those years, especially mitar.

The original idea came from the excellent work of Diggory Blake, who created the first implementation.