Last active
October 4, 2021 18:35
-
-
Save wearhere/acf71ceba51a2e77c8ec to your computer and use it in GitHub Desktop.
A small, browser-only, read-only client for a Meteor backend. More info and examples at https://mixmax.com/blog/meteor-and-backbone.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A very lightweight version of Meteor's Minimongo collections, | |
* a local store for records sent from the Meteor backend. | |
*/ | |
var LocalCollection = function() { | |
this._initialize(); | |
}; | |
_.extend(LocalCollection.prototype, Backbone.Events, { | |
_name: null, | |
_docs: null, | |
_initialize: function() { | |
this._docs = {}; | |
}, | |
_onAdded: function(id, fields) { | |
var doc = this._docs[id] = _.extend(_.clone(fields), { _id: id }); | |
this.trigger('added', doc); | |
}, | |
_onChanged: function(id, fields, cleared) { | |
var doc = this._docs[id]; | |
if (!doc) { | |
throw new Error('Document has been changed without having been added!'); | |
} | |
doc = this._docs[id] = _.chain(doc).extend(fields).omit(cleared).value(); | |
// If a field was removed from the document then it should be present in _fields_ with a | |
// value of `undefined`. | |
var changeset = _.clone(fields); | |
if (!_.isEmpty(cleared)) { | |
_.extend(changeset, | |
_.object(cleared, _.times(cleared.length, _.constant(undefined)))); | |
} | |
this.trigger('changed', doc, changeset); | |
}, | |
_onRemoved: function(id) { | |
var doc = this._docs[id]; | |
if (!doc) { | |
throw new Error('Document has been removed without having been added!'); | |
} | |
delete this._docs[id]; | |
this.trigger('removed', doc); | |
}, | |
find: function(selector) { | |
return new ReactiveQuery(this, selector); | |
}, | |
toArray: function() { | |
return _.values(this._docs); | |
} | |
}); | |
window.LocalCollection = LocalCollection; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The interface to Meteor collections and subscriptions. | |
*/ | |
var Meteor = { | |
_endpoint: null, | |
_connectedDeferred: null, | |
_loggedInDeferred: null, | |
_loginToken: null, | |
_connection: null, | |
_subscriptions: null, | |
_collections: null, | |
initialize: function(host, ssl) { | |
this._endpoint = (ssl ? "https://" : "http://") + host + "/sockjs"; | |
this._connectedDeferred = new Deferred(); | |
this._loggedInDeferred = new Deferred(); | |
this._subscriptions = {}; | |
this._collections = {}; | |
}, | |
whenConnected: function() { | |
return this._connectedDeferred.promise; | |
}, | |
whenLoggedIn: function() { | |
return this._loggedInDeferred.promise; | |
}, | |
connectUsingToken: function(loginToken) { | |
if (this._connection) throw new Error('Already connected!'); | |
this._loginToken = loginToken; | |
this._connection = new DDP({ | |
endpoint: this._endpoint, | |
SocketConstructor: SockJS | |
}); | |
this._connection.once('connected', this._onConnectionSuccess.bind(this)); | |
this._connection.once('failed', this._onConnectionFailure.bind(this)); | |
// The Meteor instance observes the collection-change callbacks so that it can create | |
// `LocalCollection` instances to receive the changes if they don't already exist. | |
this._connection.on('added', this._onAdded.bind(this)); | |
this._connection.on('changed', this._onChanged.bind(this)); | |
this._connection.on('removed', this._onRemoved.bind(this)); | |
}, | |
subscribe: function(name /*, param1, param2… optional */) { | |
var params = _.rest(arguments); | |
// Hash the name and params to cache the subscription. | |
var subscriptionKey = JSON.stringify(_.toArray(arguments)); | |
var subscription = this._subscriptions[subscriptionKey]; | |
if (!subscription) { | |
subscription = this._subscriptions[subscriptionKey] = new Subscription(name, params, this); | |
} | |
return subscription; | |
}, | |
getCollection: function(name) { | |
var collection = this._collections[name]; | |
if (!collection) { | |
collection = this._collections[name] = new LocalCollection(name, this); | |
} | |
return collection; | |
}, | |
_onConnectionSuccess: function() { | |
this._connectedDeferred.resolve(); | |
this._login(); | |
}, | |
_onConnectionFailure: function() { | |
this._connectedDeferred.reject(); | |
}, | |
_login: function() { | |
var methodId = this._connection.method('login', [{ resume: this._loginToken }]); | |
var onResult = function(message) { | |
if (message.id === methodId) { | |
this._onLoginResult(message); | |
this._connection.off('result', onResult); | |
} | |
}.bind(this); | |
this._connection.on('result', onResult); | |
}, | |
_onLoginResult: function(result) { | |
if (result.error) { | |
this._loggedInDeferred.reject(result.error); | |
} else { | |
this._loggedInDeferred.resolve(); | |
} | |
}, | |
// For some reason, document IDs may come prefixed with '-'. | |
// https://github.com/mondora/asteroid/issues/40 | |
_fixMessageId: function(message) { | |
if (message.id[0] === '-') { | |
message.id = message.id.slice(1); | |
} | |
}, | |
_onAdded: function(message) { | |
this._fixMessageId(message); | |
var collectionName = message.collection; | |
var id = message.id; | |
var fields = message.fields; | |
var collection = this.getCollection(collectionName); | |
collection._onAdded(id, fields); | |
}, | |
_onChanged: function(message) { | |
this._fixMessageId(message); | |
var collectionName = message.collection; | |
var id = message.id; | |
var fields = message.fields; | |
var cleared = message.cleared; | |
var collection = this.getCollection(collectionName); | |
collection._onChanged(id, fields, cleared); | |
}, | |
_onRemoved: function(message) { | |
this._fixMessageId(message); | |
var collectionName = message.collection; | |
var id = message.id; | |
var collection = this.getCollection(collectionName); | |
collection._onRemoved(id); | |
} | |
}; | |
window.Meteor = Meteor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Creates a reactive query. | |
* | |
* The query will emit 'added', 'changed', and 'removed' events with the same semantics as | |
* Meteor's `Mongo.Cursor.prototype.observeChanges` callbacks. | |
* | |
* @param {LocalCollection} collection - The collection to observe. | |
* @param {object} selector - The selector against which to match the records in the collection. | |
* The only form of selector supported at present is a simple set of key-value properties. | |
*/ | |
var ReactiveQuery = function(collection, selector) { | |
this._initialize(collection, selector); | |
}; | |
_.extend(ReactiveQuery.prototype, Backbone.Events, { | |
_collection: null, | |
_selector: null, | |
_initialize: function(collection, selector) { | |
this._collection = collection; | |
this._selector = selector; | |
this._collection.on('added', this._onAdded.bind(this)); | |
this._collection.on('changed', this._onChanged.bind(this)); | |
this._collection.on('removed', this._onRemoved.bind(this)); | |
}, | |
_onAdded: function(doc) { | |
if (_.isMatch(doc, this._selector)) { | |
// _fields_ should have all fields of the document excluding the `_id` field. | |
this.trigger('added', doc._id, _.omit(doc, '_id')); | |
} | |
}, | |
_onChanged: function(doc, changeset) { | |
if (_.isMatch(doc, this._selector)) { | |
this.trigger('changed', doc._id, changeset); | |
} | |
}, | |
_onRemoved: function(doc) { | |
if (_.isMatch(doc, this._selector)) { | |
this.trigger('removed', doc._id); | |
} | |
}, | |
fetch: function() { | |
return _.filter(this._collection.toArray(), _.matcher(this._selector)); | |
} | |
}); | |
window.ReactiveQuery = ReactiveQuery; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The object returned by `Meteor.subscribe`. | |
*/ | |
var Subscription = function(name, params, meteor) { | |
this._initialize(name, params, meteor); | |
}; | |
_.extend(Subscription.prototype, { | |
_meteor: null, | |
_readyDeferred: null, | |
_boundOnReady: null, | |
whenReady: function() { | |
return this._readyDeferred.promise; | |
}, | |
_initialize: function(name, params, meteor) { | |
this._meteor = meteor; | |
this._readyDeferred = new Deferred(); | |
// HACK(jeff): We _technically_ only need to be connected in order to subscribe. However, it's | |
// almost certain that the subscription won't return with any data until the user's logged in, | |
// so the 'ready' event won't be that useful unless we wait. | |
this._meteor.whenLoggedIn().then(function() { | |
this._id = this._meteor._connection.sub(name, params); | |
this._meteor._connection.on('ready', this._boundOnReady); | |
}.bind(this)); | |
this._boundOnReady = this._onReady.bind(this); | |
}, | |
_onReady: function(message) { | |
var readySubs = message.subs; | |
if (_.contains(readySubs, this._id)) { | |
this._readyDeferred.resolve(); | |
this._meteor._connection.off('ready', this._boundOnReady); | |
} | |
} | |
}); | |
window.Subscription = Subscription; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This will be renamed/polished/packaged as a full-fledge open-source project soon. In the meantime, this requires:
Backbone.Events
(possibly to be replaced by a library that just does events, though this is intended to be used with Backbone)And a Deferred implementation e.g. ES6 Promises (already implemented in all modern browsers) plus this: