├── .editorconfig ├── .eslintrc ├── .versions ├── README.md ├── client.js ├── package.js ├── server.js └── tests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 2 8 | 9 | [*.{js,html,css}] 10 | charset = utf-8 11 | 12 | [*.html] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | rules: 2 | indent: 3 | - 2 4 | - tab 5 | quotes: 6 | - 2 7 | - single 8 | linebreak-style: 9 | - 2 10 | - unix 11 | semi: 12 | - 2 13 | - never 14 | comma-dangle: 15 | - 2 16 | - 'always-multiline' 17 | no-undef: 18 | - 0 19 | no-console: 20 | - 0 21 | 22 | env: 23 | es6: true 24 | meteor: true 25 | node: true 26 | browser: true 27 | 28 | extends: 'eslint:recommended' 29 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | callback-hook@1.0.4 9 | check@1.1.0 10 | ddp@1.2.2 11 | ddp-client@1.2.1 12 | ddp-common@1.2.2 13 | ddp-server@1.2.2 14 | deps@1.0.9 15 | diff-sequence@1.0.1 16 | ecmascript@0.1.6 17 | ecmascript-runtime@0.2.6 18 | ejson@1.0.7 19 | geojson-utils@1.0.4 20 | html-tools@1.0.5 21 | htmljs@1.0.5 22 | id-map@1.0.4 23 | jquery@1.11.4 24 | local-test:ouk:static-subs@1.0.1 25 | logging@1.0.8 26 | meteor@1.1.10 27 | minimongo@1.0.10 28 | mongo@1.1.3 29 | mongo-id@1.0.1 30 | npm-mongo@1.4.39_1 31 | observe-sequence@1.0.7 32 | ordered-dict@1.0.4 33 | ouk:static-subs@1.0.1 34 | promise@0.5.1 35 | random@1.0.5 36 | reactive-var@1.0.6 37 | retry@1.0.4 38 | routepolicy@1.0.6 39 | spacebars@1.0.7 40 | spacebars-compiler@1.0.7 41 | tinytest@1.0.6 42 | tracker@1.0.9 43 | ui@1.0.8 44 | underscore@1.0.4 45 | webapp@1.2.3 46 | webapp-hashing@1.0.5 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Static subscriptions 2 | 3 | Fetch data for client just once. Any changes in database will not be reflected on client. 4 | Useful to fetch theoretically and practically static data. 5 | 6 | Signature of `Meteor.publish/subscribe` is retained. `publish` function does not have `this.userId` prop. 7 | 8 | Data received via this package is not flushed when publication gets invalidated on server. 9 | Use this package to retrieve public data only. For example, blog posts, categories of posts, any data accessible by guests. 10 | 11 | If you have subscribed to sensitive data related to `userId` then flush it by yourself. 12 | 13 | 14 | ## Installation 15 | ```sh 16 | meteor add ouk:static-subs 17 | ``` 18 | 19 | 20 | ## API 21 | 22 | Package exports `StaticSubs` variable 23 | 24 | #### `StaticSubs.publish(name, func)` 25 | Server side function to register publication 26 | ``` 27 | NOTE publication does not have `this.userId` 28 | 29 | @param {String} name Name of the record set. 30 | @param {Function} func Function called on the server each time a client subscribes. 31 | If the client passed arguments to `subscribe`, the function is called with the same arguments. 32 | ``` 33 | 34 | #### `StaticSubs.subscribe(name [, arg1, arg2, ...] [, callbacks])` 35 | Client side function to register publication 36 | ``` 37 | Returns a handle with `ready()` method to determine state of subscription. 38 | 39 | @param {String} name Name of the subscription. Matches the name of the server's `publish()` call. 40 | @param {Any} [arg1,arg2...] Optional arguments passed to publisher function on server. 41 | @param {Function|Object} [callbacks] Optional. May include `onStop` 42 | and `onReady` callbacks. If there is an error, it is passed as an 43 | argument to `onStop`. If a function is passed instead of an object, it 44 | is interpreted as an `onReady` callback. 45 | @returns {{ready: ready}} 46 | ``` 47 | 48 | 49 | ## Usage 50 | 51 | Package should be used only to publish insecure data. 52 | Do not use this package to subscribe to data related with `userId`. 53 | 54 | On server 55 | ```js 56 | StaticSubs.publish('posts', function(page) { 57 | check(page, Number) 58 | return Posts.find({}, { limit: 10, offset: page * 10 - 10 }) 59 | }) 60 | ``` 61 | 62 | On client 63 | ```js 64 | let sub = StaticSubs.subscribe('posts', 1) 65 | 66 | Tracker.autorun(() => { 67 | if (sub.ready()) { 68 | console.log(Posts.find().count()) 69 | } 70 | }) 71 | ``` 72 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | StaticSubs = {} 2 | 3 | /** 4 | * Returns a handle with `ready()` method to determine state of subscription. 5 | * 6 | * @param {String} name Name of the subscription. Matches the name of the server's `publish()` call. 7 | * @param {Any} [arg1,arg2...] Optional arguments passed to publisher function on server. 8 | * @param {Function|Object} [callbacks] Optional. May include `onStop` 9 | * and `onReady` callbacks. If there is an error, it is passed as an 10 | * argument to `onStop`. If a function is passed instead of an object, it 11 | * is interpreted as an `onReady` callback. 12 | * @returns {{ready: ready}} 13 | */ 14 | StaticSubs.subscribe = function(/* name [, arg1, arg2, ...] [, callbacks] */) { 15 | let isReady = new ReactiveVar(false) 16 | let args = _.toArray(arguments) 17 | let last = _.last(args) 18 | let cb 19 | 20 | if (_.isFunction(last) || (_.isFunction(last.onReady) || _.isFunction(last.onStop))) { 21 | args.pop() 22 | cb = last 23 | } 24 | 25 | let done = (err, resp) => { 26 | if (err) { 27 | if (cb && cb.onStop) { 28 | return cb.onStop(err) 29 | } 30 | 31 | throw err 32 | } 33 | 34 | _.each(EJSON.parse(resp), (data, colName) => { 35 | let col = Meteor.connection._stores[colName] 36 | let makeMessage = makeMessageFor(colName) 37 | data.forEach(it => col.update(makeMessage(it))) 38 | }) 39 | 40 | isReady.set(true) 41 | 42 | if (cb && cb.onReady) { 43 | cb.onReady() 44 | } 45 | else if (_.isFunction(cb)) { 46 | cb() 47 | } 48 | } 49 | 50 | Meteor.call(...['__ouk:static-subs__retrieve__', ...args, done]) 51 | 52 | return { 53 | ready: () => isReady.get(), 54 | } 55 | } 56 | 57 | let makeMessageFor = colName => it => { 58 | return { 59 | msg: 'added', 60 | collection: colName, 61 | id: it._id, 62 | fields: _.omit(it, '_id'), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ouk:static-subs', 3 | version: '1.0.1', 4 | summary: 'Static subscriptions', 5 | git: 'https://github.com/andrejsm/meteor-static-subs', 6 | documentation: 'README.md', 7 | }) 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.2.1') 11 | api.export('StaticSubs') 12 | 13 | api.use('ecmascript') 14 | api.use('underscore') 15 | api.use('reactive-var') 16 | api.use('ejson') 17 | api.use('check') 18 | 19 | api.addFiles('server.js', ['server']) 20 | api.addFiles('client.js', ['client']) 21 | }) 22 | 23 | Package.onTest(function(api) { 24 | api.use('ecmascript') 25 | api.use('tinytest') 26 | api.use('mongo') 27 | api.use('underscore') 28 | api.use('tracker') 29 | api.use('ouk:static-subs') 30 | api.addFiles('tests.js') 31 | }) 32 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | StaticSubs = {} 2 | /** 3 | * NOTE publication does not have `this.userId` 4 | * 5 | * @param {String} name Name of the record set. 6 | * @param {Function} func Function called on the server each time a client subscribes. 7 | * If the client passed arguments to `subscribe`, the function is called with the same arguments. 8 | */ 9 | StaticSubs.publish = function(name, func) { 10 | pubStore[name] = func 11 | } 12 | 13 | let pubStore = {} 14 | let toEJSON = cursor => { 15 | if (!Array.isArray(cursor)) { 16 | cursor = [cursor] 17 | } 18 | 19 | let data = {} 20 | _.each(cursor, it => data[it._cursorDescription.collectionName] = it.fetch()) 21 | 22 | return EJSON.stringify(data) 23 | } 24 | 25 | Meteor.methods({ 26 | '__ouk:static-subs__retrieve__': function() { 27 | let [ pub, ...args ] = [...arguments] 28 | try { 29 | return toEJSON(pubStore[pub](...args)) 30 | } 31 | catch (e) { 32 | throw new Meteor.Error('' + e) 33 | } 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | let CollectionX = new Mongo.Collection('x') 2 | let CollectionY = new Mongo.Collection('y') 3 | let CollectionZ = new Mongo.Collection('z') 4 | 5 | if (Meteor.isServer) { 6 | Tinytest.add('Collection setup on server', function(test) { 7 | 8 | CollectionX.remove({}) 9 | CollectionY.remove({}) 10 | CollectionZ.remove({}) 11 | 12 | _.range(5).forEach(it => CollectionX.insert({ it })) 13 | _.range(10).forEach(it => CollectionY.insert({ it })) 14 | _.range(15).forEach(it => CollectionZ.insert({ it })) 15 | 16 | test.equal(CollectionX.find().count(), 5, 'Collection X should hold 5 entries') 17 | test.equal(CollectionY.find().count(), 10, 'Collection Y should hold 10 entries') 18 | test.equal(CollectionZ.find().count(), 15, 'Collection Z should hold 15 entries') 19 | 20 | StaticSubs.publish('simplePub', function() { 21 | return CollectionX.find() 22 | }) 23 | 24 | StaticSubs.publish('combinedPub', function() { 25 | return [ 26 | CollectionY.find(), 27 | CollectionZ.find(), 28 | ] 29 | }) 30 | 31 | StaticSubs.publish('onReadyTest', function() { 32 | return [] 33 | }) 34 | }) 35 | } 36 | 37 | if (Meteor.isClient) { 38 | Tinytest.addAsync('Subscription to simplePub is successful', function(test, next) { 39 | 40 | let sub = StaticSubs.subscribe('simplePub', function() { 41 | test.equal(sub.ready(), true, 'Simple subscription is ready') 42 | test.equal(CollectionX.find().count(), 5, 'Collection X should hold 5 entries') 43 | next() 44 | }) 45 | 46 | test.equal(sub.ready(), false, 'Simple subscription is not ready') 47 | }) 48 | 49 | Tinytest.addAsync('Subscription to combinedPub is successful', function(test, next) { 50 | 51 | let sub = StaticSubs.subscribe('combinedPub') 52 | 53 | test.equal(sub.ready(), false, 'Combined subscription is not ready') 54 | 55 | Tracker.autorun(() => { 56 | if (sub.ready()) { 57 | test.equal(CollectionY.find().count(), 10, 'Collection Y should hold 10 entries') 58 | test.equal(CollectionZ.find().count(), 15, 'Collection Z should hold 15 entries') 59 | next() 60 | } 61 | }) 62 | }) 63 | 64 | Tinytest.addAsync('Subscription callback `onReady` is invoked', function(test, next) { 65 | 66 | let cb = { 67 | onReady: () => { 68 | test.equal(true, true, '`onReady` is called') 69 | test.equal(sub.ready(), true, 'onReadyTest subscription is ready') 70 | next() 71 | }, 72 | } 73 | let sub = StaticSubs.subscribe('onReadyTest', cb) 74 | 75 | test.equal(sub.ready(), false, 'onReadyTest subscription is not ready') 76 | }) 77 | 78 | Tinytest.addAsync('Fail client subscription', function(test, next) { 79 | 80 | let cb = { 81 | onStop: (err) => { 82 | test.notEqual(err, undefined, 'Error must be returned') 83 | next() 84 | }, 85 | onReady: (err) => { 86 | test.notEqual(err, undefined, 'Error must be returned') 87 | next() 88 | }, 89 | } 90 | 91 | StaticSubs.subscribe('err', cb) 92 | }) 93 | } 94 | --------------------------------------------------------------------------------