rascal

repository·master·Indexed 19 days ago

https://github.com/onebeyond/rascal

An advanced RabbitMQ / AMQP client built on amqplib. It provides high-level abstractions called Publications and Subscriptions to simplify complex messaging patterns, featuring automatic reconnection, transparent encryption, and advanced error handling. Supports both Promise-based (BrokerAsPromised) and callback-based initialization, channel pooling, and cluster connection strategies.

Tokens
18.3K
Snippets
62
Records
68
Agent score
64%

What's inside rascal

  1. Configure prefetch and channelPrefetch

    master

    Prefetch

    prefetch limits the number of unacknowledged messages a subscription can have outstanding. This prevents overloading your event loop or downstream services. The default is 10.

    Channel Prefetch

    channelPrefetch operates at the channel level. Because Rascal uses a dedicated channel per subscriber, this is rarely needed and has higher overhead in clusters. It is primarily used to adjust prefetch dynamically without cancelling the subscription.

    Note: When using channelPrefetch, you should set the regular consumer prefetch to 0 to prevent conflicts.

    // Dynamically tuning channel prefetch
    const subscription = await broker.subscribe('s1', { prefetch: 0, channelPrefetch: 5 });
    subscription.on('message', (message, content, ackOrNack) => {
      ackOrNack();
      const prefetch = tunePrefetch();
      await subscription.setChannelPrefetch(prefetch);
    });
  2. What are Publications and Subscriptions in Rascal?

    master

    Rascal extends standard RabbitMQ concepts (Brokers, Vhosts, Exchanges, Queues, Channels, and Connections) with two primary abstractions: Publications and Subscriptions.

    • Publication: A named configuration for publishing messages. It defines the destination (queue or exchange), routing configuration, encryption profiles, reliability guarantees, and message options.
    • Subscription: A named configuration for consuming messages. It defines the source queue, encryption profiles, content encoding, and delivery options (such as acknowledgement handling and prefetch).

    Both must be defined in your configuration and supplied when creating the Rascal broker. Once the broker is initialized, you retrieve these named configurations to interact with the broker.

  3. Use replyTo for stateless application instances

    master

    To allow a consumer to send a reply to the specific application instance that published a message, use the replyTo property.

    When replyTo is set to true in a queue configuration, Rascal appends a unique UUID to the queue name. When combined with the replyTo property in a publication configuration, Rascal automatically sets the replyTo property on outbound messages to that unique queue name.

    {
      "queues": {
        "q1": {
          "replyTo": true
        }
      },
      "publications": {
        "exchange": "e1",
        "replyTo": "q1"
      }
    }
  4. Use shorthand notation in Rascal configuration

    master

    Rascal supports a shorthand notation for configuration to reduce verbosity. You can use arrays of strings for simple definitions or a mix of strings and objects when specific parameters (like exchange types) are required.

    // Shorthand for exchanges, queues, and bindings
    {
      "exchanges": ["e1", "e2"],
      "queues": ["q1", "q2"],
      "bindings": ["e1 -> q1", "e2[bk1, bk2] -> q2"]
    }
    
    // Mixing shorthand with object configuration for specific parameters
    {
      "exchanges": [
        "e1",
        {
          "name": "e2",
          "type": "fanout"
        }
      ]
    }
  5. Configure Management API for vhost assertion/checking

    master

    Since AMQP doesn't support checking if a vhost exists, Rascal uses the RabbitMQ Management API. This is primarily useful in test environments.

    You can specify management details within the connection object. You can also provide a custom agent (e.g., for TLS) via the broker components during creation.

    const https = require('https');
    const agent = new https.Agent(options);
    const components = { agent };
    const broker = await Broker.create(config, components);
    {
      "vhosts": {
        "v1": {
          "connection": {
            "hostname": "broker.example.com",
            "user": "bob",
            "password": "secret",
            "management": {
              "protocol": "https",
              "pathname": "prefix",
              "user": "admin",
              "password": "super-secret",
              "options": {
                "timeout": 1000
              }
            }
          }
        }
      }
    }
  6. Use RabbitMQ Streams with Rascal

    master

    Rascal supports RabbitMQ Streams by setting the x-queue-type argument to stream in the queue options.

    Important Considerations:

    • Streams are best for high throughput where occasional message loss is tolerable (e.g., analytics).
    • You must manage data retention (messages are never deleted unless configured) and consumer offsets manually.
    • When consuming, you should store the offset in a database and use x-stream-offset in the subscription overrides to resume after restarts.
    • ackOrNack in streams does not support the error argument; call it without arguments.
      const initialOffset = (await loadOffset('/my-queue')) || 'first';
    
    const overrides = {
        options: {
          arguments: {
            'x-stream-offset': initialOffset
          }
        }
      };
    
    const subscription = await broker.subscribe('/my-queue', overrides);
    
    subscription.on('message', async (message, content, ackOrNack) => {
        const currentOffset = message.properties.headers['x-stream-offset'];
        try {
          await handleMessage(content);
          await updateOffset('/my-queue', currentOffset);
        } catch (err) {
          await handleError('/my-queue', currentOffset, err);
        } finally {
          ackOrNack(); // Streams do not support nack so do not pass the error argument
        }
      });
  7. Initialize Rascal using Callbacks

    master

    To use Rascal with the traditional callback pattern, import Broker from the rascal package. The create method accepts a configuration object and a callback function that receives the broker instance or an error.

    const Broker = require('rascal').Broker;
    const config = require('./config');
    
    Broker.create(config, (err, broker) => {
      if (err) throw err;
    
      broker.on('error', console.error);
    
      // Publish a message
      broker.publish('demo_publication', 'Hello World!', (err, publication) => {
        if (err) throw err;
        publication.on('error', console.error);
      });
    
      // Consume a message
      broker.subscribe('demo_subscription', (err, subscription) => {
        if (err) throw err;
        subscription
          .on('message', (message, content, ackOrNack) => {
            console.log(content);
            ackOrNack();
          })
          .on('error', console.error);
      });
    });
  8. Abort paused publications

    master

    Rascal uses a channel pool that pauses when the connection to the broker is lost. Instead of erroring, publishes are held in an in-memory queue. To prevent messages from being sent once the connection is re-established, listen for the paused event and call publication.abort().

    broker.publish('p1', 'some message', (err, publication) => {
      if (err) throw err;
      publication
        .on('success', (messageId) => {
          console.log('Message id was: ', messageId);
        })
        .on('error', (err, messageId) => {
          console.error('Error was: ', err.message);
        })
        .on('paused', (messageId) => {
          console.warn('Publication was paused. Aborting message: ', messageId);
          publication.abort();
        });
    });
  9. Initialize Rascal with default or test configurations

    master

    Rascal provides helper methods to merge your custom definitions with sensible defaults. Use withDefaultConfig for production-ready settings (optimized for reliability) or withTestConfig for test environments.

    var rascal = require('rascal');
    var definitions = require('./your-config.json');
    
    // For production/standard use
    var config = rascal.withDefaultConfig(definitions);
    
    // For test environments
    var config = rascal.withTestConfig(definitions);
    var rascal = require('rascal');
    var definitions = require('./your-config.json');
    var config = rascal.withDefaultConfig(definitions);
  10. Handle errors to prevent application crashes

    master

    Rascal re-emits error events from the underlying amqplib driver. If these are not handled, they will bubble up to the uncaught error handler and crash your Node.js process. To ensure both application stability and Rascal's ability to perform automatic recovery, you must register error handlers in these four locations:

    1. The Broker: Immediately after creating the broker instance.
    2. The Subscription: After calling subscribe().
    3. The Publication: After calling publish().
    4. The Forwarding: After calling forward().

    Note: Simply using a global uncaughtException handler is insufficient; it prevents the crash but also prevents Rascal from recovering.

    // 1. Broker error handler
    broker.on('error', (err, { vhost, connectionUrl }) => {
      console.error('Broker error', err, vhost, connectionUrl);
    });
    
    // 2. Subscriber error handler (Async/Await example)
    try {
      const subscription = await broker.subscribe('s1');
      subscription.on('error', (err) => {
        console.error('Subscriber error', err);
      });
    } catch (err) {
      throw new Error(`Rascal config error: ${err.message}`);
    }
    
    // 3. Publisher error handler (Async/Await example)
    try {
      const publication = await broker.publish('p1', 'some text');
      publication.on('error', (err, messageId) => {
        console.error('Publisher error', err, messageId);
      });
    } catch (err) {
      throw new Error(`Rascal config error: ${err.message}`);
    }
    
    // 4. Forwarder error handler (Async/Await example)
    try {
      const publication = await broker.forward('p1', message);
      publication.on('error', (err, messageId) => {
        console.error('Publisher error', err, messageId);
      });
    } catch (err) {
      throw new Error(`Rascal config error: ${err.message}`);
    }
  11. Initialize Rascal using Async/Await

    master

    To use Rascal with Promises and async/await, import BrokerAsPromised from the rascal package. You must provide a configuration object to the create method. It is critical to attach an error handler to the broker instance immediately to prevent application crashes from connection or channel errors.

    const Broker = require('rascal').BrokerAsPromised;
    const config = require('./config');
    
    (async () => {
      try {
        const broker = await Broker.create(config);
        broker.on('error', console.error);
    
        // Publish a message
        const publication = await broker.publish('demo_publication', 'Hello World!');
        publication.on('error', console.error);
    
        // Consume a message
        const subscription = await broker.subscribe('demo_subscription');
        subscription
          .on('message', (message, content, ackOrNack) => {
            console.log(content);
            ackOrNack();
          })
          .on('error', console.error);
      } catch (err) {
        console.error(err);
      }
    })();