├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── client ├── css │ └── styles.css ├── index.html ├── js │ └── pubsub-client.js ├── modules │ ├── app │ │ └── app.js │ ├── player │ │ ├── player.directive.js │ │ └── player.html │ ├── playlist │ │ ├── playlist.directive.js │ │ ├── playlist.html │ │ └── playlist.services.js │ ├── progress │ │ ├── progress.directive.js │ │ └── progress.html │ ├── pubsub │ │ └── pubsub.services.js │ └── song │ │ ├── new-song.html │ │ ├── song.directive.js │ │ ├── song.html │ │ └── song.services.js └── test │ ├── index.html │ └── test.controller.js ├── common └── models │ ├── song.js │ └── song.json ├── gulpfile.js ├── package.json ├── pubsub-client.js ├── pubsub-mqtt.js ├── pubsub-primus.js ├── server ├── boot │ ├── authentication.js │ ├── changes.js │ ├── primus.js │ └── pubsub-client.js ├── component-config.json ├── config.json ├── datasources.json ├── middleware.json ├── model-config.json └── server.js └── test └── ci.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Bug or feature request 10 | 11 | 14 | 15 | - [ ] Bug 16 | - [ ] Feature request 17 | 18 | ### Description of feature (or steps to reproduce if bug) 19 | 20 | 21 | 22 | ### Link to sample repo to reproduce issue (if bug) 23 | 24 | 25 | 26 | ### Expected result 27 | 28 | 29 | 30 | ### Actual result (if bug) 31 | 32 | 33 | 34 | ### Additional information (Node.js version, LoopBack version, etc) 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | #### Related issues 5 | 6 | 12 | 13 | - None 14 | 15 | ### Checklist 16 | 17 | 21 | 22 | - [ ] New tests added or existing tests modified to cover all changes 23 | - [ ] Code conforms with the [style 24 | guide](http://loopback.io/doc/en/contrib/style-guide.html) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | client/js/lb-services.js 3 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | /client/ 2 | /node_modules/ 3 | pubsub-client.js 4 | pubsub-mqtt.js 5 | pubsub-primus.js 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "nonew": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "trailing": true, 19 | "sub": true, 20 | "maxlen": 80, 21 | "globals": { 22 | // mocha 23 | "describe": false, 24 | "it": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "after": false, 28 | "afterEach": false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .project 3 | .strong-pm 4 | *.sublime-* 5 | .DS_Store 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.swp 13 | *.swo 14 | node_modules 15 | coverage 16 | *.tgz 17 | *.xml 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2015,2016. All Rights Reserved. 2 | Node module: loopback-example-pubsub 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-example-pubsub 2 | 3 | Example app for LoopBack Pub-sub. 4 | 5 | See also: 6 | - [Pub-sub documentation](http://docs.strongloop.com/display/MSG/Pub-sub) 7 | - [StrongLoop Pub-sub repository](https://github.com/strongloop/strong-pubsub) 8 | 9 | ## Install 10 | 11 | The example requires `node`, `npm` and [`mosquitto`](http://mosquitto.org/download/). 12 | 13 | ``` 14 | $ git clone https://github.com/strongloop/loopback-example-pubsub.git 15 | $ cd loopback-example-pubsub 16 | $ npm install 17 | $ gulp # manually exit via ctrl+c here, it is a known issue ATM 18 | $ mosquitto & # run the mosquitto server 19 | $ node server/server.js 20 | ``` 21 | 22 | The project is generated by [LoopBack](http://loopback.io). 23 | 24 | --- 25 | 26 | [More LoopBack examples](https://loopback.io/doc/en/lb3/Tutorials-and-examples.html) 27 | -------------------------------------------------------------------------------- /client/css/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | padding: 0; 6 | margin: 0; 7 | height: 100%; 8 | width: 100%; 9 | background-image: -moz-radial-gradient(circle, #eeeeee, #bdc3c7 80%); 10 | background-image: -webkit-radial-gradient(circle, #eeeeee, #bdc3c7 80%); 11 | background-image: radial-gradient(circle, #eeeeee, #bdc3c7 80%); 12 | font-family: 'Open Sans',sans-serif; 13 | } 14 | 15 | /*** 16 | * ps-player 17 | ***/ 18 | ps-player { 19 | width: 320px; 20 | height: 320px; 21 | display: inline-block; 22 | position: relative; 23 | box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.3); 24 | } 25 | ps-player a, ps-player a:hover, ps-player a:link, ps-player a:active { 26 | color: #fff; 27 | text-decoration: none; 28 | } 29 | ps-player p, ps-player h1 { 30 | padding: 0; 31 | margin: 0; 32 | } 33 | ps-player h1 { 34 | font-size: 1em; 35 | font-weight: 100; 36 | } 37 | ps-player p { 38 | font-size: 0.75em; 39 | } 40 | ps-player .album-art { 41 | width: 100%; 42 | height: 100%; 43 | background-size: cover; 44 | background-position: center; 45 | } 46 | ps-player .wave { 47 | position: absolute; 48 | top: 0; 49 | width: 100%; 50 | height: 100%; 51 | background-size: cover; 52 | background-position: center; 53 | background-repeat: no-repeat; 54 | opacity: 0.0; 55 | } 56 | ps-player .details { 57 | width: 100%; 58 | height: 64px; 59 | background-color: rgba(0, 0, 0, 0.5); 60 | color: #fff; 61 | } 62 | ps-player .track-info { 63 | float: left; 64 | padding-left: 4px; 65 | } 66 | ps-player .play { 67 | float: right; 68 | width: 64px; 69 | height: 64px; 70 | line-height: 64px; 71 | text-align: center; 72 | font-size: 24px; 73 | cursor: pointer; 74 | background: #2CDE50; 75 | transition: all 0.3s ease 0s; 76 | } 77 | ps-player .play:hover { 78 | background: #6be784; 79 | } 80 | ps-player .play:active { 81 | background: #27c748; 82 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.5) inset; 83 | } 84 | 85 | ps-player .playhead { 86 | background-color: rgba(0, 0, 0, 0.3); 87 | height: 5px; 88 | position: absolute; 89 | top: 5px; 90 | left: 5px; 91 | padding: 5px; 92 | line-height: 5px; 93 | } 94 | 95 | ps-progress .currentTime { 96 | background-color: rgba(0, 0, 0, 0.9); 97 | height: 18px; 98 | position: absolute; 99 | top: 5px; 100 | right: 5px; 101 | color: #fff; 102 | text-align: center; 103 | min-width: 50px; 104 | padding: 5px; 105 | line-height: 18px; 106 | } 107 | 108 | ps-progress .playing { 109 | background: #ff0000; 110 | } 111 | 112 | 113 | .add-input { 114 | display: inline-block; 115 | height: 32px; 116 | width: 320px; 117 | float: left; 118 | padding: 0; 119 | margin: 0; 120 | border: none; 121 | } 122 | 123 | .add-btn { 124 | display: inline-block; 125 | float: left; 126 | width: 32px; 127 | height: 32px; 128 | padding: 0; 129 | margin: 0; 130 | text-align: center; 131 | font-size: 24px; 132 | cursor: pointer; 133 | background: #2CDE50; 134 | transition: all 0.3s ease 0s; 135 | color: #fff; 136 | border: none; 137 | } 138 | .add-btn:hover { 139 | background: #6be784; 140 | } 141 | .add-btn:active { 142 | background: #27c748; 143 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.5) inset; 144 | } 145 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LoopBack Example PubSub 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Playlist

17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/js/pubsub-client.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var Client = require('strong-pubsub'); 7 | var Adapter = require('strong-pubsub-mqtt'); 8 | var duplex = require('duplex'); 9 | 10 | Primus.Stream = require('stream'); 11 | 12 | module.exports = function(PORT) { 13 | 14 | var client = new Client({port: PORT, host: 'localhost'}, Adapter, { 15 | createConnection: function(port, host) { 16 | var connection = duplex(); 17 | 18 | var primus = Primus.connect('http://localhost:3000', { 19 | transformer: 'engine.io', 20 | parser: 'binary' 21 | }); 22 | 23 | connection.on('_data', function(chunk) { 24 | // someone called `connection.write(buf)` 25 | primus.write(chunk); 26 | }); 27 | 28 | primus.on('data', function(chunk) { 29 | if (chunk && !Buffer.isBuffer(chunk)) { 30 | // chunk is an arrayBuffer 31 | connection._data(toBuffer(chunk)); 32 | } 33 | }); 34 | 35 | primus.on('open', function() { 36 | connection.emit('connect'); 37 | }); 38 | 39 | connection.on('_end', function() { 40 | primus.end(); 41 | this._end(); 42 | }); 43 | 44 | return connection; 45 | } 46 | }); 47 | 48 | return client; 49 | } 50 | 51 | 52 | function toBuffer(ab) { 53 | var buffer = new Buffer(ab.byteLength); 54 | var view = new Uint8Array(ab); 55 | for(var i = 0; i < buffer.length; ++i) { 56 | buffer[i] = view[i]; 57 | } 58 | return buffer; 59 | } 60 | -------------------------------------------------------------------------------- /client/modules/app/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var app = angular.module('ps', ['lbServices']); 7 | 8 | app.controller('AppController', function($scope, Playlist) { 9 | var playlistId; 10 | 11 | if(window.location.hash) { 12 | playlistId = window.location.hash.replace('#', ''); 13 | } 14 | 15 | var playlist = $scope.playlist = new Playlist(playlistId); 16 | 17 | window.location.hash = playlist.id; 18 | 19 | // update the playlist 20 | playlist.getSongs(); 21 | }); 22 | -------------------------------------------------------------------------------- /client/modules/player/player.directive.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.directive('psPlayer', function ($http, $interval, Song) { 7 | function link(scope) { 8 | var clientid = 'b23455855ab96a4556cbd0a98397ae8c'; 9 | var song = scope.song; 10 | 11 | if(!song) return; 12 | 13 | $http({ 14 | method: 'GET', 15 | url: 'https://api.soundcloud.com/resolve.json?client_id='+ clientid + '&url=' + song.scURL 16 | }) 17 | .success(function (trackData) { 18 | $http({ 19 | method: 'GET', 20 | url: 'http://api.soundcloud.com/tracks/'+trackData.id+'.json?client_id='+clientid 21 | }) 22 | .success(function (data) { 23 | scope.band = data.user.username; 24 | scope.bandUrl = data.user.permalink_url; 25 | scope.title = data.title; 26 | scope.trackUrl = data.permalink_url; 27 | if(data.artwork_url) { 28 | scope.albumArt = data.artwork_url.replace("large", "t500x500"); 29 | } 30 | scope.wave = data.waveform_url; 31 | scope.stream = data.stream_url + '?client_id=' + clientid; 32 | 33 | var audio = scope.audio = new Audio(); 34 | audio.onended = function() { 35 | scope.onEnded(); 36 | } 37 | audio.ontimeupdate = function() { 38 | scope.onProgress(); 39 | scope.progress(); 40 | } 41 | 42 | onPlayChanged(); 43 | }); 44 | }); 45 | 46 | song.playing = song.playing || false; 47 | 48 | // watch song.playing 49 | scope.$watch('song.playing', function(newPlaying, oldPlaying) { 50 | if(newPlaying !== oldPlaying) { 51 | onPlayChanged(); 52 | } 53 | }); 54 | 55 | scope.togglePlay = function() { 56 | song.playing = !song.playing; 57 | console.log('updateAttributes', song.id); 58 | 59 | Song.prototype$updateAttributes({ id: song.id }, { 60 | playing: song.playing 61 | }); 62 | } 63 | 64 | function onPlayChanged() { 65 | if (song.playing) { 66 | if (!scope.audio.src) { 67 | scope.audio.src = scope.stream; 68 | } 69 | scope.audio.play(); 70 | 71 | if (scope.onPlay) { 72 | scope.onPlay(); 73 | } 74 | } else { 75 | scope.audio.pause(); 76 | 77 | if (scope.onPause) { 78 | scope.onPause(); 79 | } 80 | } 81 | } 82 | 83 | scope.formatTime = function(time) { 84 | var minutes = Math.floor(time / 60); 85 | var seconds = Math.round(time - minutes * 60); 86 | 87 | if(minutes < 10) { 88 | minutes = '0' + minutes; 89 | } 90 | if(seconds < 10) { 91 | seconds = '0' + seconds; 92 | } 93 | return minutes + ':' + seconds; 94 | } 95 | 96 | scope.currentTime = 0; 97 | 98 | scope.progress = function() { 99 | scope.currentTime = scope.audio.currentTime; 100 | scope.duration = scope.audio.duration; 101 | scope.$apply(); 102 | } 103 | } 104 | return { 105 | restrict: 'E', 106 | scope: { 107 | song: '=', 108 | onPlay: '&', 109 | onPause: '&', 110 | onProgress: '&', 111 | onEnded: '&' 112 | }, 113 | templateUrl: '/modules/player/player.html', 114 | link: link 115 | }; 116 | }); 117 | -------------------------------------------------------------------------------- /client/modules/player/player.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
{{formatTime(currentTime)}}
6 |
7 |
8 |
9 |

{{title}}

10 |

{{band}}

11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /client/modules/playlist/playlist.directive.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.directive('psPlaylist', function ($http, $interval) { 7 | function link(scope) { 8 | } 9 | return { 10 | restrict: 'E', 11 | scope: { 12 | playlist: '=' 13 | }, 14 | templateUrl: '/modules/playlist/playlist.html', 15 | link: link 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /client/modules/playlist/playlist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/modules/playlist/playlist.services.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.factory('Playlist', function($http, Song, pubsubClient) { 7 | function Playlist(id) { 8 | var playlist = this; 9 | this.id = id || generateId(); 10 | this.songs = []; 11 | 12 | this.subscribe(); 13 | 14 | pubsubClient.on('message', function(topic, msg) { 15 | var id; 16 | if(topic.indexOf(playlist.getTopic()) === 0) { 17 | id = parseInt(msg.toString()); 18 | playlist.onSongChanged(id); 19 | } 20 | }); 21 | } 22 | 23 | Playlist.prototype.onSongChanged = function(id) { 24 | var songExists = false; 25 | var playlist = this; 26 | 27 | this.songs.forEach(function(song) { 28 | if(song.id === id) { 29 | songExists = true; 30 | playlist.updateSong(song); 31 | } 32 | }); 33 | 34 | if(!songExists) { 35 | playlist.addSongById(id); 36 | } 37 | } 38 | 39 | Playlist.prototype.updateSong = function(song) { 40 | console.log('updateSong'); 41 | Song.findById({ 42 | id: song.id 43 | }, function(latest) { 44 | Object.keys(latest).forEach(function(key) { 45 | // update in place (angular will update the ui) 46 | song[key] = latest[key]; 47 | }); 48 | }); 49 | } 50 | 51 | Playlist.prototype.addSongById = function(id) { 52 | var playlist = this; 53 | 54 | Song.findById({ 55 | id: id 56 | }, function(song) { 57 | playlist.songs.push(song); 58 | }); 59 | } 60 | 61 | Playlist.prototype.addSong = function(url) { 62 | var id = this.id; 63 | var song = { 64 | playlist: id, 65 | scURL: url 66 | }; 67 | var playlist = this; 68 | 69 | 70 | return Song.create(song, function(createdSong) { 71 | playlist.songs.push(createdSong); 72 | }); 73 | } 74 | 75 | Playlist.prototype.getSongs = function() { 76 | this.songs = Song.find({ 77 | filter: {where: {playlist: this.id}} 78 | }); 79 | } 80 | 81 | Playlist.prototype.subscribe = function() { 82 | pubsubClient.subscribe(this.getTopic()); 83 | } 84 | 85 | Playlist.prototype.getTopic = function() { 86 | return '/playlists/' + this.id; 87 | } 88 | 89 | function generateId() { 90 | var n = Math.random(); 91 | return n.toString().split('.')[1].toString(); 92 | } 93 | 94 | return Playlist; 95 | }); 96 | -------------------------------------------------------------------------------- /client/modules/progress/progress.directive.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.directive('psProgress', function ($http, $interval) { 7 | function link(scope) { 8 | console.log('song', scope.song); 9 | scope.formatTime = function(time) { 10 | var minutes = Math.floor(time / 60); 11 | var seconds = Math.round(time - minutes * 60); 12 | 13 | if(minutes < 10) { 14 | minutes = '0' + minutes; 15 | } 16 | if(seconds < 10) { 17 | seconds = '0' + seconds; 18 | } 19 | return minutes + ':' + seconds; 20 | } 21 | } 22 | return { 23 | restrict: 'E', 24 | scope: { 25 | song: '=' 26 | }, 27 | templateUrl: '/modules/progress/progress.html', 28 | link: link 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /client/modules/progress/progress.html: -------------------------------------------------------------------------------- 1 |
2 |
{{formatTime(song.currentTime)}}
3 | -------------------------------------------------------------------------------- /client/modules/pubsub/pubsub.services.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.factory('pubsubClient', function() { 7 | return require('pubsub-client')(3000); 8 | }); 9 | -------------------------------------------------------------------------------- /client/modules/song/new-song.html: -------------------------------------------------------------------------------- 1 |

Enter a URL for a SoundCloud track:

2 | 3 | + 4 | -------------------------------------------------------------------------------- /client/modules/song/song.directive.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.directive('psSong', function () { 7 | function link(scope) { 8 | } 9 | return { 10 | restrict: 'E', 11 | scope: { 12 | song: '=' 13 | }, 14 | templateUrl: '/modules/song/song.html', 15 | link: link 16 | }; 17 | }); 18 | 19 | app.directive('psNewSong', function () { 20 | function link(scope) { 21 | scope.add = function() { 22 | var playlist = scope.playlist; 23 | 24 | playlist.addSong(scope.url); 25 | 26 | delete scope.url; 27 | } 28 | } 29 | return { 30 | restrict: 'E', 31 | scope: { 32 | playlist: '=' 33 | }, 34 | templateUrl: '/modules/song/new-song.html', 35 | link: link 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /client/modules/song/song.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/modules/song/song.services.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.factory('Song', function() { 7 | function Song(url) { 8 | this.url = url; 9 | } 10 | 11 | Song.prototype.getMeta = function() { 12 | return $http.get(this.url); 13 | } 14 | 15 | return Song; 16 | }); 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LoopBack Example PubSub 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |

Player

19 | 20 | 21 |
22 | 23 |

Song

24 | 25 | 26 |
27 | 28 |

Playlist

29 | 30 | 31 | 32 |
33 | 34 |

New Song

35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /client/test/test.controller.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | app.controller('TestController', ['$scope', 'Playlist', function($scope, Playlist) { 7 | var testURL = 'https://soundcloud.com/cosmoknot/to-u'; 8 | 9 | $scope.testSongs = [$scope.testSong]; 10 | 11 | var playlist = $scope.playlist = new Playlist('1234567'); 12 | 13 | playlist.addSong(testURL); 14 | 15 | }]); 16 | -------------------------------------------------------------------------------- /common/models/song.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | module.exports = function(Song) { 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /common/models/song.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Song", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "properties": {}, 6 | "validations": [], 7 | "relations": {}, 8 | "acls": [], 9 | "methods": [] 10 | } 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var gulp = require('gulp'); 7 | var rename = require('gulp-rename'); 8 | var loopbackAngular = require('gulp-loopback-sdk-angular'); 9 | 10 | gulp.task('default', function() { 11 | return gulp.src('./server/server.js') 12 | .pipe(loopbackAngular()) 13 | .pipe(rename('lb-services.js')) 14 | .pipe(gulp.dest('./client/js')); 15 | }); 16 | 17 | gulp.doneCallback = function(err) { 18 | process.exit(err ? 1 : 0); 19 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-example-pubsub", 3 | "version": "1.0.0", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "lint": "jshint .", 7 | "start": "node .", 8 | "test": "mocha", 9 | "posttest": "npm run lint" 10 | }, 11 | "dependencies": { 12 | "binary-pack": "0.0.3", 13 | "compression": "^1.0.3", 14 | "debug": "^2.1.3", 15 | "duplex": "^1.0.0", 16 | "engine.io": "^1.5.1", 17 | "strong-error-handler": "^1.1.0", 18 | "loopback": "^3.0.0", 19 | "loopback-boot": "^2.4.0", 20 | "primus": "2.4.12", 21 | "serve-favicon": "^2.0.1", 22 | "strong-pubsub": "^0.1.0", 23 | "strong-pubsub-connection-mqtt": "^0.1.0", 24 | "strong-pubsub-mqtt": "^0.1.0", 25 | "strong-pubsub-primus": "^0.1.0", 26 | "strong-pubsub-proxy": "^0.1.0", 27 | "loopback-component-explorer": "^2.1.0" 28 | }, 29 | "devDependencies": { 30 | "browserify": "^11.0.1", 31 | "gulp": "^3.8.11", 32 | "gulp-loopback-sdk-angular": "^0.1.3", 33 | "gulp-rename": "^1.2.2", 34 | "jshint": "^2.5.6", 35 | "mocha": "^2.3.4" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/strongloop/loopback-example-pubsub" 40 | }, 41 | "description": "strong-pubsub-angular-loopback", 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /pubsub-client.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.PubSubClient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && this._events[type].length > m) { 371 | this._events[type].warned = true; 372 | console.error('(node) warning: possible EventEmitter memory ' + 373 | 'leak detected. %d listeners added. ' + 374 | 'Use emitter.setMaxListeners() to increase limit.', 375 | this._events[type].length); 376 | if (typeof console.trace === 'function') { 377 | // not supported in IE 10 378 | console.trace(); 379 | } 380 | } 381 | } 382 | 383 | return this; 384 | }; 385 | 386 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 387 | 388 | EventEmitter.prototype.once = function(type, listener) { 389 | if (!isFunction(listener)) 390 | throw TypeError('listener must be a function'); 391 | 392 | var fired = false; 393 | 394 | function g() { 395 | this.removeListener(type, g); 396 | 397 | if (!fired) { 398 | fired = true; 399 | listener.apply(this, arguments); 400 | } 401 | } 402 | 403 | g.listener = listener; 404 | this.on(type, g); 405 | 406 | return this; 407 | }; 408 | 409 | // emits a 'removeListener' event iff the listener was removed 410 | EventEmitter.prototype.removeListener = function(type, listener) { 411 | var list, position, length, i; 412 | 413 | if (!isFunction(listener)) 414 | throw TypeError('listener must be a function'); 415 | 416 | if (!this._events || !this._events[type]) 417 | return this; 418 | 419 | list = this._events[type]; 420 | length = list.length; 421 | position = -1; 422 | 423 | if (list === listener || 424 | (isFunction(list.listener) && list.listener === listener)) { 425 | delete this._events[type]; 426 | if (this._events.removeListener) 427 | this.emit('removeListener', type, listener); 428 | 429 | } else if (isObject(list)) { 430 | for (i = length; i-- > 0;) { 431 | if (list[i] === listener || 432 | (list[i].listener && list[i].listener === listener)) { 433 | position = i; 434 | break; 435 | } 436 | } 437 | 438 | if (position < 0) 439 | return this; 440 | 441 | if (list.length === 1) { 442 | list.length = 0; 443 | delete this._events[type]; 444 | } else { 445 | list.splice(position, 1); 446 | } 447 | 448 | if (this._events.removeListener) 449 | this.emit('removeListener', type, listener); 450 | } 451 | 452 | return this; 453 | }; 454 | 455 | EventEmitter.prototype.removeAllListeners = function(type) { 456 | var key, listeners; 457 | 458 | if (!this._events) 459 | return this; 460 | 461 | // not listening for removeListener, no need to emit 462 | if (!this._events.removeListener) { 463 | if (arguments.length === 0) 464 | this._events = {}; 465 | else if (this._events[type]) 466 | delete this._events[type]; 467 | return this; 468 | } 469 | 470 | // emit removeListener for all listeners on all events 471 | if (arguments.length === 0) { 472 | for (key in this._events) { 473 | if (key === 'removeListener') continue; 474 | this.removeAllListeners(key); 475 | } 476 | this.removeAllListeners('removeListener'); 477 | this._events = {}; 478 | return this; 479 | } 480 | 481 | listeners = this._events[type]; 482 | 483 | if (isFunction(listeners)) { 484 | this.removeListener(type, listeners); 485 | } else { 486 | // LIFO order 487 | while (listeners.length) 488 | this.removeListener(type, listeners[listeners.length - 1]); 489 | } 490 | delete this._events[type]; 491 | 492 | return this; 493 | }; 494 | 495 | EventEmitter.prototype.listeners = function(type) { 496 | var ret; 497 | if (!this._events || !this._events[type]) 498 | ret = []; 499 | else if (isFunction(this._events[type])) 500 | ret = [this._events[type]]; 501 | else 502 | ret = this._events[type].slice(); 503 | return ret; 504 | }; 505 | 506 | EventEmitter.listenerCount = function(emitter, type) { 507 | var ret; 508 | if (!emitter._events || !emitter._events[type]) 509 | ret = 0; 510 | else if (isFunction(emitter._events[type])) 511 | ret = 1; 512 | else 513 | ret = emitter._events[type].length; 514 | return ret; 515 | }; 516 | 517 | function isFunction(arg) { 518 | return typeof arg === 'function'; 519 | } 520 | 521 | function isNumber(arg) { 522 | return typeof arg === 'number'; 523 | } 524 | 525 | function isObject(arg) { 526 | return typeof arg === 'object' && arg !== null; 527 | } 528 | 529 | function isUndefined(arg) { 530 | return arg === void 0; 531 | } 532 | 533 | },{}],3:[function(require,module,exports){ 534 | if (typeof Object.create === 'function') { 535 | // implementation from standard node.js 'util' module 536 | module.exports = function inherits(ctor, superCtor) { 537 | ctor.super_ = superCtor 538 | ctor.prototype = Object.create(superCtor.prototype, { 539 | constructor: { 540 | value: ctor, 541 | enumerable: false, 542 | writable: true, 543 | configurable: true 544 | } 545 | }); 546 | }; 547 | } else { 548 | // old school shim for old browsers 549 | module.exports = function inherits(ctor, superCtor) { 550 | ctor.super_ = superCtor 551 | var TempCtor = function () {} 552 | TempCtor.prototype = superCtor.prototype 553 | ctor.prototype = new TempCtor() 554 | ctor.prototype.constructor = ctor 555 | } 556 | } 557 | 558 | },{}],4:[function(require,module,exports){ 559 | // shim for using process in browser 560 | 561 | var process = module.exports = {}; 562 | var queue = []; 563 | var draining = false; 564 | 565 | function drainQueue() { 566 | if (draining) { 567 | return; 568 | } 569 | draining = true; 570 | var currentQueue; 571 | var len = queue.length; 572 | while(len) { 573 | currentQueue = queue; 574 | queue = []; 575 | var i = -1; 576 | while (++i < len) { 577 | currentQueue[i](); 578 | } 579 | len = queue.length; 580 | } 581 | draining = false; 582 | } 583 | process.nextTick = function (fun) { 584 | queue.push(fun); 585 | if (!draining) { 586 | setTimeout(drainQueue, 0); 587 | } 588 | }; 589 | 590 | process.title = 'browser'; 591 | process.browser = true; 592 | process.env = {}; 593 | process.argv = []; 594 | process.version = ''; // empty string to avoid regexp issues 595 | process.versions = {}; 596 | 597 | function noop() {} 598 | 599 | process.on = noop; 600 | process.addListener = noop; 601 | process.once = noop; 602 | process.off = noop; 603 | process.removeListener = noop; 604 | process.removeAllListeners = noop; 605 | process.emit = noop; 606 | 607 | process.binding = function (name) { 608 | throw new Error('process.binding is not supported'); 609 | }; 610 | 611 | // TODO(shtylman) 612 | process.cwd = function () { return '/' }; 613 | process.chdir = function (dir) { 614 | throw new Error('process.chdir is not supported'); 615 | }; 616 | process.umask = function() { return 0; }; 617 | 618 | },{}],5:[function(require,module,exports){ 619 | module.exports = function isBuffer(arg) { 620 | return arg && typeof arg === 'object' 621 | && typeof arg.copy === 'function' 622 | && typeof arg.fill === 'function' 623 | && typeof arg.readUInt8 === 'function'; 624 | } 625 | },{}],6:[function(require,module,exports){ 626 | (function (process,global){ 627 | // Copyright Joyent, Inc. and other Node contributors. 628 | // 629 | // Permission is hereby granted, free of charge, to any person obtaining a 630 | // copy of this software and associated documentation files (the 631 | // "Software"), to deal in the Software without restriction, including 632 | // without limitation the rights to use, copy, modify, merge, publish, 633 | // distribute, sublicense, and/or sell copies of the Software, and to permit 634 | // persons to whom the Software is furnished to do so, subject to the 635 | // following conditions: 636 | // 637 | // The above copyright notice and this permission notice shall be included 638 | // in all copies or substantial portions of the Software. 639 | // 640 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 641 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 642 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 643 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 644 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 645 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 646 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 647 | 648 | var formatRegExp = /%[sdj%]/g; 649 | exports.format = function(f) { 650 | if (!isString(f)) { 651 | var objects = []; 652 | for (var i = 0; i < arguments.length; i++) { 653 | objects.push(inspect(arguments[i])); 654 | } 655 | return objects.join(' '); 656 | } 657 | 658 | var i = 1; 659 | var args = arguments; 660 | var len = args.length; 661 | var str = String(f).replace(formatRegExp, function(x) { 662 | if (x === '%%') return '%'; 663 | if (i >= len) return x; 664 | switch (x) { 665 | case '%s': return String(args[i++]); 666 | case '%d': return Number(args[i++]); 667 | case '%j': 668 | try { 669 | return JSON.stringify(args[i++]); 670 | } catch (_) { 671 | return '[Circular]'; 672 | } 673 | default: 674 | return x; 675 | } 676 | }); 677 | for (var x = args[i]; i < len; x = args[++i]) { 678 | if (isNull(x) || !isObject(x)) { 679 | str += ' ' + x; 680 | } else { 681 | str += ' ' + inspect(x); 682 | } 683 | } 684 | return str; 685 | }; 686 | 687 | 688 | // Mark that a method should not be used. 689 | // Returns a modified function which warns once by default. 690 | // If --no-deprecation is set, then it is a no-op. 691 | exports.deprecate = function(fn, msg) { 692 | // Allow for deprecating things in the process of starting up. 693 | if (isUndefined(global.process)) { 694 | return function() { 695 | return exports.deprecate(fn, msg).apply(this, arguments); 696 | }; 697 | } 698 | 699 | if (process.noDeprecation === true) { 700 | return fn; 701 | } 702 | 703 | var warned = false; 704 | function deprecated() { 705 | if (!warned) { 706 | if (process.throwDeprecation) { 707 | throw new Error(msg); 708 | } else if (process.traceDeprecation) { 709 | console.trace(msg); 710 | } else { 711 | console.error(msg); 712 | } 713 | warned = true; 714 | } 715 | return fn.apply(this, arguments); 716 | } 717 | 718 | return deprecated; 719 | }; 720 | 721 | 722 | var debugs = {}; 723 | var debugEnviron; 724 | exports.debuglog = function(set) { 725 | if (isUndefined(debugEnviron)) 726 | debugEnviron = process.env.NODE_DEBUG || ''; 727 | set = set.toUpperCase(); 728 | if (!debugs[set]) { 729 | if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { 730 | var pid = process.pid; 731 | debugs[set] = function() { 732 | var msg = exports.format.apply(exports, arguments); 733 | console.error('%s %d: %s', set, pid, msg); 734 | }; 735 | } else { 736 | debugs[set] = function() {}; 737 | } 738 | } 739 | return debugs[set]; 740 | }; 741 | 742 | 743 | /** 744 | * Echos the value of a value. Trys to print the value out 745 | * in the best way possible given the different types. 746 | * 747 | * @param {Object} obj The object to print out. 748 | * @param {Object} opts Optional options object that alters the output. 749 | */ 750 | /* legacy: obj, showHidden, depth, colors*/ 751 | function inspect(obj, opts) { 752 | // default options 753 | var ctx = { 754 | seen: [], 755 | stylize: stylizeNoColor 756 | }; 757 | // legacy... 758 | if (arguments.length >= 3) ctx.depth = arguments[2]; 759 | if (arguments.length >= 4) ctx.colors = arguments[3]; 760 | if (isBoolean(opts)) { 761 | // legacy... 762 | ctx.showHidden = opts; 763 | } else if (opts) { 764 | // got an "options" object 765 | exports._extend(ctx, opts); 766 | } 767 | // set default options 768 | if (isUndefined(ctx.showHidden)) ctx.showHidden = false; 769 | if (isUndefined(ctx.depth)) ctx.depth = 2; 770 | if (isUndefined(ctx.colors)) ctx.colors = false; 771 | if (isUndefined(ctx.customInspect)) ctx.customInspect = true; 772 | if (ctx.colors) ctx.stylize = stylizeWithColor; 773 | return formatValue(ctx, obj, ctx.depth); 774 | } 775 | exports.inspect = inspect; 776 | 777 | 778 | // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics 779 | inspect.colors = { 780 | 'bold' : [1, 22], 781 | 'italic' : [3, 23], 782 | 'underline' : [4, 24], 783 | 'inverse' : [7, 27], 784 | 'white' : [37, 39], 785 | 'grey' : [90, 39], 786 | 'black' : [30, 39], 787 | 'blue' : [34, 39], 788 | 'cyan' : [36, 39], 789 | 'green' : [32, 39], 790 | 'magenta' : [35, 39], 791 | 'red' : [31, 39], 792 | 'yellow' : [33, 39] 793 | }; 794 | 795 | // Don't use 'blue' not visible on cmd.exe 796 | inspect.styles = { 797 | 'special': 'cyan', 798 | 'number': 'yellow', 799 | 'boolean': 'yellow', 800 | 'undefined': 'grey', 801 | 'null': 'bold', 802 | 'string': 'green', 803 | 'date': 'magenta', 804 | // "name": intentionally not styling 805 | 'regexp': 'red' 806 | }; 807 | 808 | 809 | function stylizeWithColor(str, styleType) { 810 | var style = inspect.styles[styleType]; 811 | 812 | if (style) { 813 | return '\u001b[' + inspect.colors[style][0] + 'm' + str + 814 | '\u001b[' + inspect.colors[style][1] + 'm'; 815 | } else { 816 | return str; 817 | } 818 | } 819 | 820 | 821 | function stylizeNoColor(str, styleType) { 822 | return str; 823 | } 824 | 825 | 826 | function arrayToHash(array) { 827 | var hash = {}; 828 | 829 | array.forEach(function(val, idx) { 830 | hash[val] = true; 831 | }); 832 | 833 | return hash; 834 | } 835 | 836 | 837 | function formatValue(ctx, value, recurseTimes) { 838 | // Provide a hook for user-specified inspect functions. 839 | // Check that value is an object with an inspect function on it 840 | if (ctx.customInspect && 841 | value && 842 | isFunction(value.inspect) && 843 | // Filter out the util module, it's inspect function is special 844 | value.inspect !== exports.inspect && 845 | // Also filter out any prototype objects using the circular check. 846 | !(value.constructor && value.constructor.prototype === value)) { 847 | var ret = value.inspect(recurseTimes, ctx); 848 | if (!isString(ret)) { 849 | ret = formatValue(ctx, ret, recurseTimes); 850 | } 851 | return ret; 852 | } 853 | 854 | // Primitive types cannot have properties 855 | var primitive = formatPrimitive(ctx, value); 856 | if (primitive) { 857 | return primitive; 858 | } 859 | 860 | // Look up the keys of the object. 861 | var keys = Object.keys(value); 862 | var visibleKeys = arrayToHash(keys); 863 | 864 | if (ctx.showHidden) { 865 | keys = Object.getOwnPropertyNames(value); 866 | } 867 | 868 | // IE doesn't make error fields non-enumerable 869 | // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx 870 | if (isError(value) 871 | && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { 872 | return formatError(value); 873 | } 874 | 875 | // Some type of object without properties can be shortcutted. 876 | if (keys.length === 0) { 877 | if (isFunction(value)) { 878 | var name = value.name ? ': ' + value.name : ''; 879 | return ctx.stylize('[Function' + name + ']', 'special'); 880 | } 881 | if (isRegExp(value)) { 882 | return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); 883 | } 884 | if (isDate(value)) { 885 | return ctx.stylize(Date.prototype.toString.call(value), 'date'); 886 | } 887 | if (isError(value)) { 888 | return formatError(value); 889 | } 890 | } 891 | 892 | var base = '', array = false, braces = ['{', '}']; 893 | 894 | // Make Array say that they are Array 895 | if (isArray(value)) { 896 | array = true; 897 | braces = ['[', ']']; 898 | } 899 | 900 | // Make functions say that they are functions 901 | if (isFunction(value)) { 902 | var n = value.name ? ': ' + value.name : ''; 903 | base = ' [Function' + n + ']'; 904 | } 905 | 906 | // Make RegExps say that they are RegExps 907 | if (isRegExp(value)) { 908 | base = ' ' + RegExp.prototype.toString.call(value); 909 | } 910 | 911 | // Make dates with properties first say the date 912 | if (isDate(value)) { 913 | base = ' ' + Date.prototype.toUTCString.call(value); 914 | } 915 | 916 | // Make error with message first say the error 917 | if (isError(value)) { 918 | base = ' ' + formatError(value); 919 | } 920 | 921 | if (keys.length === 0 && (!array || value.length == 0)) { 922 | return braces[0] + base + braces[1]; 923 | } 924 | 925 | if (recurseTimes < 0) { 926 | if (isRegExp(value)) { 927 | return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); 928 | } else { 929 | return ctx.stylize('[Object]', 'special'); 930 | } 931 | } 932 | 933 | ctx.seen.push(value); 934 | 935 | var output; 936 | if (array) { 937 | output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); 938 | } else { 939 | output = keys.map(function(key) { 940 | return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); 941 | }); 942 | } 943 | 944 | ctx.seen.pop(); 945 | 946 | return reduceToSingleString(output, base, braces); 947 | } 948 | 949 | 950 | function formatPrimitive(ctx, value) { 951 | if (isUndefined(value)) 952 | return ctx.stylize('undefined', 'undefined'); 953 | if (isString(value)) { 954 | var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') 955 | .replace(/'/g, "\\'") 956 | .replace(/\\"/g, '"') + '\''; 957 | return ctx.stylize(simple, 'string'); 958 | } 959 | if (isNumber(value)) 960 | return ctx.stylize('' + value, 'number'); 961 | if (isBoolean(value)) 962 | return ctx.stylize('' + value, 'boolean'); 963 | // For some reason typeof null is "object", so special case here. 964 | if (isNull(value)) 965 | return ctx.stylize('null', 'null'); 966 | } 967 | 968 | 969 | function formatError(value) { 970 | return '[' + Error.prototype.toString.call(value) + ']'; 971 | } 972 | 973 | 974 | function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { 975 | var output = []; 976 | for (var i = 0, l = value.length; i < l; ++i) { 977 | if (hasOwnProperty(value, String(i))) { 978 | output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, 979 | String(i), true)); 980 | } else { 981 | output.push(''); 982 | } 983 | } 984 | keys.forEach(function(key) { 985 | if (!key.match(/^\d+$/)) { 986 | output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, 987 | key, true)); 988 | } 989 | }); 990 | return output; 991 | } 992 | 993 | 994 | function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { 995 | var name, str, desc; 996 | desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; 997 | if (desc.get) { 998 | if (desc.set) { 999 | str = ctx.stylize('[Getter/Setter]', 'special'); 1000 | } else { 1001 | str = ctx.stylize('[Getter]', 'special'); 1002 | } 1003 | } else { 1004 | if (desc.set) { 1005 | str = ctx.stylize('[Setter]', 'special'); 1006 | } 1007 | } 1008 | if (!hasOwnProperty(visibleKeys, key)) { 1009 | name = '[' + key + ']'; 1010 | } 1011 | if (!str) { 1012 | if (ctx.seen.indexOf(desc.value) < 0) { 1013 | if (isNull(recurseTimes)) { 1014 | str = formatValue(ctx, desc.value, null); 1015 | } else { 1016 | str = formatValue(ctx, desc.value, recurseTimes - 1); 1017 | } 1018 | if (str.indexOf('\n') > -1) { 1019 | if (array) { 1020 | str = str.split('\n').map(function(line) { 1021 | return ' ' + line; 1022 | }).join('\n').substr(2); 1023 | } else { 1024 | str = '\n' + str.split('\n').map(function(line) { 1025 | return ' ' + line; 1026 | }).join('\n'); 1027 | } 1028 | } 1029 | } else { 1030 | str = ctx.stylize('[Circular]', 'special'); 1031 | } 1032 | } 1033 | if (isUndefined(name)) { 1034 | if (array && key.match(/^\d+$/)) { 1035 | return str; 1036 | } 1037 | name = JSON.stringify('' + key); 1038 | if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { 1039 | name = name.substr(1, name.length - 2); 1040 | name = ctx.stylize(name, 'name'); 1041 | } else { 1042 | name = name.replace(/'/g, "\\'") 1043 | .replace(/\\"/g, '"') 1044 | .replace(/(^"|"$)/g, "'"); 1045 | name = ctx.stylize(name, 'string'); 1046 | } 1047 | } 1048 | 1049 | return name + ': ' + str; 1050 | } 1051 | 1052 | 1053 | function reduceToSingleString(output, base, braces) { 1054 | var numLinesEst = 0; 1055 | var length = output.reduce(function(prev, cur) { 1056 | numLinesEst++; 1057 | if (cur.indexOf('\n') >= 0) numLinesEst++; 1058 | return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; 1059 | }, 0); 1060 | 1061 | if (length > 60) { 1062 | return braces[0] + 1063 | (base === '' ? '' : base + '\n ') + 1064 | ' ' + 1065 | output.join(',\n ') + 1066 | ' ' + 1067 | braces[1]; 1068 | } 1069 | 1070 | return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; 1071 | } 1072 | 1073 | 1074 | // NOTE: These type checking functions intentionally don't use `instanceof` 1075 | // because it is fragile and can be easily faked with `Object.create()`. 1076 | function isArray(ar) { 1077 | return Array.isArray(ar); 1078 | } 1079 | exports.isArray = isArray; 1080 | 1081 | function isBoolean(arg) { 1082 | return typeof arg === 'boolean'; 1083 | } 1084 | exports.isBoolean = isBoolean; 1085 | 1086 | function isNull(arg) { 1087 | return arg === null; 1088 | } 1089 | exports.isNull = isNull; 1090 | 1091 | function isNullOrUndefined(arg) { 1092 | return arg == null; 1093 | } 1094 | exports.isNullOrUndefined = isNullOrUndefined; 1095 | 1096 | function isNumber(arg) { 1097 | return typeof arg === 'number'; 1098 | } 1099 | exports.isNumber = isNumber; 1100 | 1101 | function isString(arg) { 1102 | return typeof arg === 'string'; 1103 | } 1104 | exports.isString = isString; 1105 | 1106 | function isSymbol(arg) { 1107 | return typeof arg === 'symbol'; 1108 | } 1109 | exports.isSymbol = isSymbol; 1110 | 1111 | function isUndefined(arg) { 1112 | return arg === void 0; 1113 | } 1114 | exports.isUndefined = isUndefined; 1115 | 1116 | function isRegExp(re) { 1117 | return isObject(re) && objectToString(re) === '[object RegExp]'; 1118 | } 1119 | exports.isRegExp = isRegExp; 1120 | 1121 | function isObject(arg) { 1122 | return typeof arg === 'object' && arg !== null; 1123 | } 1124 | exports.isObject = isObject; 1125 | 1126 | function isDate(d) { 1127 | return isObject(d) && objectToString(d) === '[object Date]'; 1128 | } 1129 | exports.isDate = isDate; 1130 | 1131 | function isError(e) { 1132 | return isObject(e) && 1133 | (objectToString(e) === '[object Error]' || e instanceof Error); 1134 | } 1135 | exports.isError = isError; 1136 | 1137 | function isFunction(arg) { 1138 | return typeof arg === 'function'; 1139 | } 1140 | exports.isFunction = isFunction; 1141 | 1142 | function isPrimitive(arg) { 1143 | return arg === null || 1144 | typeof arg === 'boolean' || 1145 | typeof arg === 'number' || 1146 | typeof arg === 'string' || 1147 | typeof arg === 'symbol' || // ES6 symbol 1148 | typeof arg === 'undefined'; 1149 | } 1150 | exports.isPrimitive = isPrimitive; 1151 | 1152 | exports.isBuffer = require('./support/isBuffer'); 1153 | 1154 | function objectToString(o) { 1155 | return Object.prototype.toString.call(o); 1156 | } 1157 | 1158 | 1159 | function pad(n) { 1160 | return n < 10 ? '0' + n.toString(10) : n.toString(10); 1161 | } 1162 | 1163 | 1164 | var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 1165 | 'Oct', 'Nov', 'Dec']; 1166 | 1167 | // 26 Feb 16:19:34 1168 | function timestamp() { 1169 | var d = new Date(); 1170 | var time = [pad(d.getHours()), 1171 | pad(d.getMinutes()), 1172 | pad(d.getSeconds())].join(':'); 1173 | return [d.getDate(), months[d.getMonth()], time].join(' '); 1174 | } 1175 | 1176 | 1177 | // log is just a thin wrapper to console.log that prepends a timestamp 1178 | exports.log = function() { 1179 | console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); 1180 | }; 1181 | 1182 | 1183 | /** 1184 | * Inherit the prototype methods from one constructor into another. 1185 | * 1186 | * The Function.prototype.inherits from lang.js rewritten as a standalone 1187 | * function (not on Function.prototype). NOTE: If this file is to be loaded 1188 | * during bootstrapping this function needs to be rewritten using some native 1189 | * functions as prototype setup using normal JavaScript does not work as 1190 | * expected during bootstrapping (see mirror.js in r114903). 1191 | * 1192 | * @param {function} ctor Constructor function which needs to inherit the 1193 | * prototype. 1194 | * @param {function} superCtor Constructor function to inherit prototype from. 1195 | */ 1196 | exports.inherits = require('inherits'); 1197 | 1198 | exports._extend = function(origin, add) { 1199 | // Don't do anything if add isn't an object 1200 | if (!add || !isObject(add)) return origin; 1201 | 1202 | var keys = Object.keys(add); 1203 | var i = keys.length; 1204 | while (i--) { 1205 | origin[keys[i]] = add[keys[i]]; 1206 | } 1207 | return origin; 1208 | }; 1209 | 1210 | function hasOwnProperty(obj, prop) { 1211 | return Object.prototype.hasOwnProperty.call(obj, prop); 1212 | } 1213 | 1214 | }).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 1215 | },{"./support/isBuffer":5,"_process":4,"inherits":3}]},{},[1])(1) 1216 | }); -------------------------------------------------------------------------------- /server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | module.exports = function enableAuthentication(server) { 7 | // enable authentication 8 | server.enableAuth(); 9 | }; 10 | -------------------------------------------------------------------------------- /server/boot/changes.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var Client = require('strong-pubsub'); 7 | var Adapter = require('strong-pubsub-mqtt'); 8 | var MOSQUITTO_PORT = process.env.MOSQUITTO_PORT || 1883; 9 | 10 | module.exports = function(app) { 11 | var Song = app.models.Song; 12 | var client = new Client({port: MOSQUITTO_PORT}, Adapter); 13 | 14 | Song.observe('after save', function updateTimestamp(ctx, next) { 15 | var song = ctx.instance; 16 | if (song) { 17 | client.publish('/playlists/' + song.playlist, song.id.toString()); 18 | } 19 | next(); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /server/boot/primus.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var Primus = require('primus'); 7 | var Client = require('strong-pubsub'); 8 | var Connection = require('strong-pubsub-connection-mqtt'); 9 | var PubSubProxy = require('strong-pubsub-proxy'); 10 | var Adapter = require('strong-pubsub-mqtt'); 11 | var MOSQUITTO_PORT = process.env.MOSQUITTO_PORT || 1883; 12 | 13 | module.exports = function(app) { 14 | app.on('started', function(server) { 15 | var primus = new Primus(server, { 16 | transformer: 'engine.io', 17 | parser: 'binary' 18 | }); 19 | 20 | primus.on('connection', function(spark) { 21 | var client = new Client({port: MOSQUITTO_PORT}, Adapter); 22 | var proxy = new PubSubProxy( 23 | new Connection(spark), 24 | client 25 | ); 26 | proxy.connect(); 27 | }); 28 | }); 29 | 30 | var testClient = new Client({port: MOSQUITTO_PORT}, Adapter); 31 | 32 | setInterval(function() { 33 | testClient.publish('/my-topic', 'hello'); 34 | }, 1000); 35 | }; 36 | -------------------------------------------------------------------------------- /server/boot/pubsub-client.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var path = require('path'); 7 | var browserify = require('browserify'); 8 | 9 | module.exports = function(app) { 10 | app.get('/pubsub-client.js', function(req, res) { 11 | var b = browserify({ 12 | basedir: __dirname, 13 | debug: true 14 | }); 15 | 16 | b.require(path.join(__dirname, '..', '..', 17 | 'client', 'js', 'pubsub-client.js'), {expose: 'pubsub-client'}); 18 | 19 | b.bundle().pipe(res); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "legacyExplorer": false, 6 | "remoting": { 7 | "context": false, 8 | "rest": { 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false, 21 | "handleErrors": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {}, 7 | "cors": { 8 | "params": { 9 | "origin": true, 10 | "credentials": true, 11 | "maxAge": 86400 12 | } 13 | } 14 | }, 15 | "session": { 16 | }, 17 | "auth": { 18 | }, 19 | "parse": { 20 | }, 21 | "routes": { 22 | "loopback#rest": { 23 | "paths": ["${restApiRoot}"] 24 | } 25 | }, 26 | "files": { 27 | }, 28 | "final": { 29 | "loopback#urlNotFound": {} 30 | }, 31 | "final:after": { 32 | "strong-error-handler": {} 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ] 9 | }, 10 | "User": { 11 | "dataSource": "db" 12 | }, 13 | "AccessToken": { 14 | "dataSource": "db", 15 | "public": false 16 | }, 17 | "ACL": { 18 | "dataSource": "db", 19 | "public": false 20 | }, 21 | "RoleMapping": { 22 | "dataSource": "db", 23 | "public": false 24 | }, 25 | "Role": { 26 | "dataSource": "db", 27 | "public": false 28 | }, 29 | "Song": { 30 | "dataSource": "db", 31 | "public": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var loopback = require('loopback'); 7 | var boot = require('loopback-boot'); 8 | 9 | var app = module.exports = loopback(); 10 | 11 | // Bootstrap the application, configure models, datasources and middleware. 12 | // Sub-apps like REST API are mounted via boot scripts. 13 | boot(app, __dirname); 14 | 15 | app.use(loopback.static(require('path').join(__dirname, '..', 'client'))); 16 | 17 | app.start = function() { 18 | // start the web server 19 | var server = app.listen(function() { 20 | app.emit('started', server); 21 | var baseUrl = app.get('url').replace(/\/$/, ''); 22 | console.log('Web server listening at: %s', app.get('url')); 23 | if (app.get('loopback-component-explorer')) { 24 | var explorerPath = app.get('loopback-component-explorer').mountPath; 25 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 26 | } 27 | }); 28 | return server; 29 | }; 30 | 31 | // start the server if `$ node server.js` 32 | if (require.main === module) { 33 | app.start(); 34 | } 35 | -------------------------------------------------------------------------------- /test/ci.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-pubsub 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | describe('ci', function() { 7 | it('should pass a basic smoke test', function(done) { 8 | done(); 9 | }); 10 | }); 11 | --------------------------------------------------------------------------------