@vrangan: thanks for the PMs discussing this.
You have already identified some areas of complexity:
- Giving users the ability (even if indirectly) to initiate cron jobs on the server
- How to cancel those jobs when the user goes away
- How to re-use data if the same van is being tracked.
The following code addresses all those concerns and does not use cron.
Assumptions
Base Collections
- A collection for packages (tracking number, status, van number, customer id, etc.)
- Other collections may be required (vans, customers, etc.)
Key Requirements
- Tracking is initiated by the customer.
- If no customers are tracking packages, no van location data is retrieved.
- Tracking will be updated every 2 seconds (but see also the code annotation, below).
- When vans are being tracked by multiple customers, GPS transponder requests will be throttled.
Suggested Implementation
I’ve made no attempt to:
- address how the tracking number is obtained from the user
- apply security checks to the method (is this user allowed to track this package?)
- show any routing
- show any sensible error handling
- show how to present a moving marker on a map
- use JSDoc annotation conventions (sorry - should have tried harder!)
Client Template (a .js file in the client/ folder)
Template.tracking.onCreated(function() {
var self = this;
self.current = new ReactiveDict();
function getLocation() {
// packageNumber needs to come from somewhere
Meteor.call('track', packageNumber, function(error, result) {
if (error) {
// Do something if we get here because it broke in a bad way!
} else {
self.current.set(result);
if (result.retry) {
// We need to rerun in "retry" ms
Meteor.setTimeout(getLocation, result.retry);
}
}
});
};
getLocation(); // This runs once on creation and then every "retry" ms
});
Template.tracking.onRendered(function() {
var self = this;
self.autorun(function() { // This will rerun whenever lat and/or long and/or status change.
// It's probably where your map marker rendering should live.
var lat = self.current.get('lat');
var long = self.current.get('long');
var status = self.current.get('status');
// do something with that stuff ...
});
});
Template.tracking.helpers({ // Not strictly necessary, but may be useful for Blaze templating
unknown: function() {
return Template.instance().current.get('status') === 'Unknown';
},
out: function() {
return Template.instance().current.get('status') === 'Out';
},
depot: function() {
return Template.instance().current.get('status') === 'Depot';
},
delivered: function() {
return Template.instance().current.get('status') === 'Delivered';
},
lat: function() {
return Template.instance().current.get('lat');
},
long: function() {
return Template.instance().current.get('long');
}
});
Server (a .js file in the server/ directory)
var Vans = {}; // We don't need on-disk persistence, so don't really need a collection, just a list of van objects.
// Remember, these will be shareable between and persist across the whole set of users as long as
// the app is running. Also much faster than (mini)mongo finds.
var getVanLocation = function(vanObj) { // Polls the van's GPS transponder for its position
// The van object from Vans[vanNumber]:
// vanNumber: The vanNumber
// time: The last time this van's position was collected
// lat: The latitude co-ordinate
// long: The longitude co-ordinate
// polling: internal flag
try {
// This may not be how you request the location!
var result = HTTP.call(... vanObj.vanNumber ...);
// and this may not be how you parse the result!
// Note also, that this section (4 following vanObj lines) may be in a callback off the HTTP.call if that's preferred.
vanObj.polling = false; // Reset ready for next poll
vanObj.lat = result.content.lat; // Position
vanObj.long = result.content.long;
vanObj.time = Date.now(); // We need this to avoid spamming for van location polling
} catch (error) {
vanObj.polling = false; // Allows another poll attempt
throw new Meteor.Error('httpError', 'Could not poll the GPS transponder');
});
var whereIsMyVan = function(vanNumber) { // Debounces incoming requests
var latency = 200; // Typical latency for polling a GPS transponder (ms)
if (! Vans[vanNumber]) { // Not seen a request for this van before, so get set up
Vans[vanNumber] = {
vanNumber: vanNumber, // We'll need this for the getVanLocation function
polling: true, // Used to indicate a van is being polled for its location
lat: null, // Dummy co-ordinates
long: null
};
getVanLocation(Vans[vanNumber]); // Request van's location
} else if (Vans[vanNumber].polling) { // Van position request already in progress - return last position.
return Vans[vanNumber]; // Even if this is null, it will self-correct on the next request
} else if (Date.now() - Vans[vanNumber].time > 2000 - latency) {
// Last position older than 2-latency secs - get new one (this is what does the "debouncing").
// Note that the latency adjustment is only needed when the number of polling clients is very small (1-3).
// Even then, with one client and no latency adjustment, the worst case refresh on the client will be 4 seconds.
Vans[vanNumber].polling = true; // Indicate we're polling the van's GPS transponder
getVanLocation(Vans[vanNumber]);
}
return Vans[vanNumber]; // However we got here, return what we've got
});
Meteor.methods({ // If you require the user to be logged in, this is the place to ensure that's been done.
track: function(trackingNumber) {
var retry = null;
var vanObj = {};
try {
var package = Packages.findOne({trackingNumber: trackingNumber});
// Get the package info for the tracking number.
// This code assumes a package document contains at least:
// trackingNumber: the customer's tracking number for this package
// status: 'Out', 'Depot', 'Delivered'
// vanNumber: the allocated van number for this package
if (package) {
if (package.status === 'Out') { // Out for delivery
retry = 2000; // The client should retry in 2 secs
vanObj = whereIsMyVan(package.vanNumber); // Update the results
} else if (package.status === 'Depot') { // Still in the depot
retry = 60000; // The client should try again in 1 minute
}
// Return the location data. Note that lat and long may be null on the first call for this van.
// The client may need to cope with that.
return {
status: package.status,
retry: retry,
lat: vanObj.lat,
long: vanObj.long
};
} else {
return {
status: 'Unknown'
};
}
} catch (error) {
throw new Meteor.Error('Error', 'Something nasty happened');
}
}
});
I’ve run this code in a small app, where I’ve simulated the HTTP.call
with a Meteor.setTimeout
- i.e. asynchronous and having significant latency, with multiple connected clients and all works as expected.
I used the reactive-dict
package for storing reactive template state.