Merge FlowRouter with Iron Router subscription controllers?

After hitting new issues with Iron Router and Blaze 3.0 (see latest entries in Iron Router work with meteor 3, I spend some time investigating what a good way forward could be. Fixing multi-region support would require serious rework of Iron Router middleware was the conclusion, hence with the help of Claude the below proposal has been crafted, which might enrich FlowRouter with simpler subscription management and make life easier for Iron Router legacy users to migrate.

All comments welcome ! Extra ideas or things I overlooked could be added before attempting to implement this.

IronFlow: FlowRouter + Iron Controller Hybrid System

Overview

IronFlow is a proposed hybrid routing solution that combines FlowRouter’s modern async-friendly architecture with Iron Router’s elegant subscription management patterns. This approach addresses the incompatibilities between Iron Router and Blaze 3.0+ while preserving the developer experience that makes Iron Router powerful.

Background

The Problem

  • Iron Router + Blaze 3.0+: Iron Router’s synchronous middleware stack conflicts with Blaze 3.0’s async architecture (Fibers removal)
  • FlowRouter limitations: FlowRouter lacks Iron Router’s controller pattern and centralized subscription management
  • Migration complexity: Moving from Iron Router to FlowRouter requires rewriting subscription handling across the entire application

The Solution

Extract Iron Router’s subscription management components and integrate them with FlowRouter’s routing system, creating a hybrid that offers:

  • Iron Router’s familiar controller patterns (waitOn, subscriptions, ready())
  • FlowRouter’s async-compatible architecture
  • Progressive migration path from existing Iron Router applications

Architecture Analysis

Iron Router Components (Extractable)

1. WaitList System (ironcontroller/lib/wait_list.js)

// Reactive readiness tracking
WaitList.prototype.wait = function (fn) {
  // Adds functions or subscription handles to wait list
};

WaitList.prototype.ready = function () {
  // Returns true when all items in wait list are ready
  return this._readyDep.get();
};

Key Features:

  • Reactive dependency tracking
  • Handles subscription arrays and boolean functions
  • Independent of routing system
  • Supports subscription handles with .ready() method

2. Controller Base Class (ironcontroller/lib/controller_client.js)

Controller.prototype.wait = function (fn) {
  // Delegates to internal WaitList
  this._waitlist.wait(fn);
};

Controller.prototype.ready = function () {
  // Reactive readiness state
  return this._waitlist.ready();
};

Controller.prototype.subscribe = function (/* same as Meteor.subscribe */) {
  // Enhanced Meteor.subscribe with .wait() method
  var handle = Meteor.subscribe.apply(this, arguments);
  return _.extend(handle, {
    wait: function () { self.wait(this); }
  });
};

Key Features:

  • ReactiveDict for component state
  • Layout rendering capabilities
  • Subscription handle enhancement
  • Lifecycle management

3. Hook Collection System (route_controller.js:108)

RouteController.prototype._collectHooks = function (/* hook1, alias1, ... */) {
  // Inheritance-aware hook gathering
  // Priority: router global -> route options -> controller prototype -> controller instance
  return routerHooks.concat(protoHooks).concat(thisHooks).concat(routeHooks);
};

Key Features:

  • Supports multiple hook types (waitOn, subscriptions, onBeforeAction, etc.)
  • Inheritance chain traversal
  • Router global hooks
  • Route-specific hooks

FlowRouter Strengths

1. Async-Compatible Architecture

  • No Fiber dependencies
  • Promise-based routing
  • Compatible with Blaze 3.0+ async patterns

2. Flexible Rendering

  • Works with any template system
  • BlazeLayout integration
  • Server-side rendering support

3. Trigger System

FlowRouter.triggers.enter([function(context, redirect) {
  // Route entry hooks
}]);

FlowRouter.triggers.exit([function(context, redirect) {
  // Route exit hooks
}]);

Proposed IronFlow Implementation

1. FlowController Class

// packages/ironflow/lib/flow_controller.js
FlowController = function(options) {
  // Inherit Iron Controller's subscription management
  Controller.call(this, options);

  // FlowRouter-specific reactive state
  this.params = new ReactiveVar({});
  this.queryParams = new ReactiveVar({});
  this.routeName = new ReactiveVar('');

  // Hook execution tracking
  this._hooksExecuted = false;
  this._currentContext = null;
};

// Inherit Iron Controller's proven subscription management
FlowController.prototype = Object.create(Controller.prototype);
FlowController.prototype.constructor = FlowController;

// FlowRouter-specific parameter management
FlowController.prototype.setParams = function(params) {
  this.params.set(params);
  return this;
};

FlowController.prototype.getParams = function() {
  this.params.depend && this.params.depend();
  return this.params.get();
};

FlowController.prototype.setQueryParams = function(queryParams) {
  this.queryParams.set(queryParams);
  return this;
};

FlowController.prototype.getQueryParams = function() {
  this.queryParams.depend && this.queryParams.depend();
  return this.queryParams.get();
};

// Context management for hook execution
FlowController.prototype.setContext = function(context) {
  this._currentContext = context;
  this.setParams(context.params);
  this.setQueryParams(context.queryParams);
  this.routeName.set(context.route.name);
};

// Execute Iron Router style hooks
FlowController.prototype.executeHooks = function() {
  if (this._hooksExecuted) return;

  var self = this;

  // Execute subscriptions hooks
  var subsList = this._collectHooks('subscriptions');
  _.each(subsList, function (subFunc) {
    self.wait(subFunc.call(self));
  });

  // Execute waitOn hooks
  var waitOnList = this._collectHooks('waitOn');
  _.each(waitOnList, function (waitOn) {
    self.wait(waitOn.call(self));
  });

  this._hooksExecuted = true;
};

// Reset for route change
FlowController.prototype.reset = function() {
  this.stop(); // Stop existing subscriptions
  this._waitlist = new WaitList;
  this._hooksExecuted = false;
  this._currentContext = null;
};

2. FlowRouter Integration

// packages/ironflow/lib/flow_router_integration.js

// Global controller registry
IronFlow = {
  controllers: {},

  // Register a controller for a route
  controller: function(routeName, controllerOptions) {
    this.controllers[routeName] = new FlowController(controllerOptions);
    return this.controllers[routeName];
  },

  // Get controller for current route
  current: function() {
    var routeName = FlowRouter.getRouteName();
    return this.controllers[routeName];
  }
};

// Global route entry trigger
FlowRouter.triggers.enter([function(context, redirect) {
  var controller = IronFlow.controllers[context.route.name];

  if (controller) {
    // Set up controller context
    controller.setContext(context);

    // Execute subscription hooks
    controller.executeHooks();

    // Store current controller globally
    Session.set('currentController', context.route.name);
  }
}]);

// Global route exit trigger
FlowRouter.triggers.exit([function(context) {
  var controller = IronFlow.controllers[context.route.name];

  if (controller) {
    // Clean up controller
    controller.reset();
  }

  Session.set('currentController', null);
}]);

// Template helper for accessing controller
Template.registerHelper('controller', function() {
  var controllerName = Session.get('currentController');
  return controllerName ? IronFlow.controllers[controllerName] : null;
});

// Template helper for readiness
Template.registerHelper('subsReady', function() {
  var controller = IronFlow.current();
  return controller ? controller.ready() : true;
});

3. Enhanced BlazeLayout Integration

// packages/ironflow/lib/blaze_layout_integration.js

// Enhanced BlazeLayout with loading awareness
IronFlow.render = function(layoutTemplate, regions) {
  var controller = IronFlow.current();

  if (controller && !controller.ready()) {
    // Render loading template
    var loadingTemplate = controller.lookupOption('loadingTemplate') || 'DefaultLoading';
    BlazeLayout.render(loadingTemplate);

    // Set up reactive rendering when ready
    Tracker.autorun(function(comp) {
      if (controller.ready()) {
        comp.stop();
        BlazeLayout.render(layoutTemplate, regions);
      }
    });
  } else {
    // Render immediately
    BlazeLayout.render(layoutTemplate, regions);
  }
};

// Default loading template
Template.DefaultLoading = new Template('DefaultLoading', function() {
  return '<div class="loading">Loading...</div>';
});

4. Route Definition Syntax

// Enhanced route definition with controller pattern
FlowRouter.route('/presence/:groupId', {
  name: 'presence',
  action: function(params, queryParams) {
    IronFlow.render('MainLayout', {
      main: 'PresencePage'
    });
  }
});

// Register controller separately for cleaner separation
IronFlow.controller('presence', {
  // Iron Router style subscription management
  waitOn: function() {
    return [
      Meteor.subscribe('presenceData', this.getParams().groupId),
      Meteor.subscribe('members', this.getParams().groupId)
    ];
  },

  subscriptions: function() {
    // Non-blocking subscriptions
    return Meteor.subscribe('realtime', this.getParams().groupId);
  },

  data: function() {
    return {
      members: Members.find({groupId: this.getParams().groupId}),
      groupId: this.getParams().groupId
    };
  },

  // Loading template override
  loadingTemplate: 'PresenceLoading',

  // Hooks
  onBeforeAction: function() {
    if (!Meteor.userId()) {
      FlowRouter.go('login');
      return false;
    }
  },

  onAfterAction: function() {
    document.title = 'Presence - ' + this.getParams().groupId;
  }
});

5. Template Integration

<!-- Using controller data in templates -->
<template name="PresencePage">
  {{#if subsReady}}
    {{#with controller.getData}}
      <h1>Group: {{groupId}}</h1>
      {{#each members}}
        <div class="member">{{name}}</div>
      {{/each}}
    {{/with}}
  {{else}}
    <div class="loading">Loading presence data...</div>
  {{/if}}
</template>

<!-- Access controller directly -->
<template name="PresenceStats">
  {{#with controller}}
    <p>Ready: {{ready}}</p>
    <p>Params: {{getParams.groupId}}</p>
    <p>Query: {{getQueryParams.filter}}</p>
  {{/with}}
</template>

Migration Strategy

Phase 1: Parallel Implementation

// Install FlowRouter alongside Iron Router
meteor add kadira:flow-router
meteor add kadira:blaze-layout

// Create IronFlow package
// Keep existing Iron Router routes working

Phase 2: Route-by-Route Migration

// Convert simple routes first
// Iron Router
Router.route('/simple', function() {
  this.render('SimplePage');
});

// IronFlow equivalent
FlowRouter.route('/simple', {
  name: 'simple',
  action: function() {
    BlazeLayout.render('MainLayout', {main: 'SimplePage'});
  }
});

Phase 3: Complex Routes with Subscriptions

// Iron Router
Router.route('/complex/:id', {
  waitOn: function() {
    return Meteor.subscribe('items', this.params.id);
  },
  action: function() {
    this.render('ComplexPage');
  }
});

// IronFlow equivalent
FlowRouter.route('/complex/:id', {
  name: 'complex',
  action: function() {
    IronFlow.render('MainLayout', {main: 'ComplexPage'});
  }
});

IronFlow.controller('complex', {
  waitOn: function() {
    return Meteor.subscribe('items', this.getParams().id);
  }
});

Phase 4: Remove Iron Router

// Once all routes migrated
meteor remove iron:router

Advantages

1. Familiar Developer Experience

  • Same patterns: waitOn, subscriptions, ready(), data()
  • Same syntax: Hook functions with this context
  • Same lifecycle: Controller-based subscription management

2. Technical Benefits

  • Async compatibility: No Fiber dependencies
  • Blaze 3.0+ support: Works with modern Blaze versions
  • Performance: FlowRouter’s lighter routing engine
  • Flexibility: Works with any template system

3. Migration Benefits

  • Progressive: Migrate routes incrementally
  • Risk reduction: Keep critical routes on Iron Router during transition
  • Parallel operation: Both systems can coexist
  • Code reuse: Existing controller logic largely unchanged

Disadvantages and Limitations

1. Development Complexity

  • Two routing systems: Temporary complexity during migration
  • Learning curve: Developers need to understand both systems
  • Debugging: Two different execution paths

2. Feature Gaps

  • Multi-region layouts: Complex region management may need custom implementation
  • Server-side routing: Iron Router’s server routing features not included
  • Layout inheritance: May need custom implementation

3. Maintenance Overhead

  • Custom package: IronFlow would need ongoing maintenance
  • Meteor compatibility: Need to track both FlowRouter and Blaze changes
  • Community support: Limited compared to established solutions

Implementation Considerations

1. Package Structure

packages/
  ironflow/
    package.js
    lib/
      flow_controller.js          # Core controller class
      flow_router_integration.js  # FlowRouter hooks
      blaze_layout_integration.js # Enhanced rendering
      wait_list.js               # Extracted from iron:controller
      reactive_dict.js           # State management
    templates/
      loading.html               # Default loading template
    tests/
      controller_tests.js
      integration_tests.js

2. Dependencies

// package.js
Package.describe({
  name: 'ironflow',
  version: '1.0.0',
  summary: 'FlowRouter + Iron Controller hybrid system'
});

Package.onUse(function(api) {
  api.versionsFrom('2.8');

  // Core dependencies
  api.use(['ecmascript', 'tracker', 'reactive-var', 'reactive-dict']);

  // FlowRouter integration
  api.use(['kadira:flow-router', 'kadira:blaze-layout']);

  // Iron Controller components (extracted)
  api.use(['iron:controller', 'iron:layout']);

  // Template system
  api.use(['templating', 'blaze-html-templates']);
});

3. Testing Strategy

// Test controller lifecycle
Tinytest.add('IronFlow - Controller lifecycle', function(test) {
  var controller = new FlowController({
    waitOn: function() {
      return Meteor.subscribe('test');
    }
  });

  // Test hook execution
  controller.executeHooks();
  test.isFalse(controller.ready());

  // Simulate subscription ready
  // Test ready state change
});

// Test FlowRouter integration
Tinytest.add('IronFlow - Route integration', function(test) {
  FlowRouter.route('/test', {name: 'test'});
  IronFlow.controller('test', {
    waitOn: function() {
      return Meteor.subscribe('test');
    }
  });

  // Test route navigation triggers controller
  FlowRouter.go('/test');
  test.isTrue(IronFlow.current() instanceof FlowController);
});

Performance Characteristics

1. Memory Usage

  • Controller instances: One per route (similar to Iron Router)
  • Reactive dependencies: WaitList, ReactiveVar, ReactiveDict per controller
  • Cleanup: Automatic on route exit

2. Rendering Performance

  • Loading states: Immediate loading template rendering
  • Reactive updates: Efficient subscription readiness tracking
  • Template caching: BlazeLayout handles template reuse

3. Network Efficiency

  • Subscription management: Same as Iron Router (proven efficient)
  • Route changes: FlowRouter’s optimized change detection
  • Data reactivity: Maintains Meteor’s reactive data system

Conclusion

IronFlow represents a practical solution to the Iron Router + Blaze 3.0 compatibility crisis. By extracting Iron Router’s proven subscription management patterns and integrating them with FlowRouter’s modern architecture, it provides:

  1. Immediate solution: Resolves Blaze 3.0 compatibility issues
  2. Familiar patterns: Preserves developer experience and existing knowledge
  3. Migration path: Allows gradual transition from Iron Router
  4. Future-proof: Built on FlowRouter’s sustainable foundation

The hybrid approach acknowledges that Iron Router’s controller pattern and subscription management are genuinely valuable innovations that shouldn’t be discarded, while embracing FlowRouter’s technical advantages for long-term sustainability.

Recommendation: For applications heavily invested in Iron Router patterns, IronFlow offers the most practical migration path that preserves both functionality and developer productivity while ensuring compatibility with modern Meteor and Blaze versions.

1 Like