Hybrid View

  1. #1
    Sencha User
    Join Date
    Jun 2007
    Posts
    46
    Vote Rating
    6
    kesteb is on a distinguished road

      1  

    Default A remote storage priovide for state management

    A remote storage priovide for state management


    I am experimenting with the state management stuff and decided that the provided CookieProvider and LocalStoarageProvider were rather limiting. So I developed a RemoteStorageProvider that uses a store to manage the interactions. Here is the code:

    PHP Code:
    /*
     * File: RemoteStorageProvider.js
     * Date: 20-Jul-2011
     * By  : Kevin L. Esteb
     *
     * This module provides a state provider that uses remote storage as
     * the backing store.
     *
     * GNU General Public License Usage
     *
     * This file may be used under the terms of the GNU General Public
     * License version 3.0 as published by the Free Software Foundation.
     * Please review the following information to ensure that the GNU
     * General Public License version 3.0 requirements will be met:
     * http://www.gnu.org/copyleft/gpl.html.
     *
     */

    Ext.define('Ext.ux.state.RemoteStorageProvider', {
        
    extend'Ext.state.Provider',
        
    mixins: {
            
    observable'Ext.util.Observable'
        
    },
        
    uses: [
            
    'Ext.data.Model',
            
    'Ext.data.Store',
            
    'Ext.state.Provider',
            
    'Ext.util.Observable'
        
    ],

        
    store: {},

        
    constructor: function(config) {

           
    config config || {};
           
    this.initialConfig config;

           
    Ext.apply(thisconfig);

           
    this.mixins.observable.constructor.call(thisconfig);

           
    this.store this.storeage();
           
    this.store.load();

        },

        
    set: function(namevalue) {
            var 
    posrow;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    row this.store.getAt(pos);
                
    row.set('value'this.encodeValue(value));

            } else {

                
    this.store.add({
                    
    namename,
                    
    valuethis.encodeValue(value)
                });

            }

            
    this.store.sync();
            
    this.fireEvent('statechange'thisnamevalue);

        },

        
    get: function(namedefaultValue) {
            var 
    posrow;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    row this.store.getAt(pos);
                return 
    this.decodeValue(row.get('value'));

            } else {

                return 
    defaultValue;

            }

        },

        
    clear: function(name) {
            var 
    pos;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    this.store.removeAt(pos);
                
    this.store.sync();
                
    this.fireEvent('statechange'thisnamenull);

            }

        },

        
    storeageExt.emptyFn

    }); 
    To use this code you need to extend the above class in the following fashion:

    PHP Code:
    /*
     * File: DesktopProvider.js
     * Date: 19-Jul-2011
     * By  : Kevin L. Esteb
     *
     * This module provides a state provider for the desktop using remote storage.
     *
     */

    Ext.define('XAS.desktop.state.DesktopProvider', {
        
    extend'Ext.ux.state.RemoteStorageProvider',
        
    uses: [
            
    'Ext.data.Model',
            
    'Ext.data.Store',
            
    'Ext.ux.state.RemoteStorageProvider'
        
    ],

        
    app_rootp'/',

        
    storeage: function() {

            
    Ext.define('XAS.desktop.model.State', {
                
    extend'Ext.data.Model',
                
    idProperty'id',
                
    fields: [
                    { 
    name'id',    type'int' },
                    { 
    name'name',  type'string' },
                    { 
    name'value'type'string' }
                ],
                
    proxy: {
                    
    api: {
                        
    create:  this.app_rootp 'desktop/state/create',
                        
    read:    this.app_rootp 'desktop/state/read',
                        
    update:  this.app_rootp 'desktop/state/update',
                        
    destroythis.app_rootp 'desktop/state/delete'
                    
    },
                    
    type'ajax',
                    
    reader: {
                        
    type'json',
                        
    root'data',
                        
    totalProperty'count',
                        
    successProperty'success'
                    
    },
                    
    writer: {
                        
    type'json',
                        
    root'data',
                        
    encodetrue
                    
    }
                }
            });

            return new 
    Ext.data.Store({
                
    model'XAS.desktop.model.State'
            
    });

        }

    }); 
    And of course you start the state management like this:

    PHP Code:

    Ext
    .state.Manager.setProvider(
        
    Ext.create('XAS.desktop.state.DesktopProvider', {
            
    app_roopme.app_rootp
        
    })
     ); 
    And of course the back end code is left as an exercise for the reader. But the backing store at least needs "id", "name" and "value" fields to work correctly.

  2. #2
    Sencha - Community Support Team edspencer's Avatar
    Join Date
    Jan 2009
    Location
    Palo Alto, California
    Posts
    1,939
    Vote Rating
    7
    edspencer is a jewel in the rough edspencer is a jewel in the rough edspencer is a jewel in the rough

      0  

    Default


    Gorgeous. Do you have examples of it running?
    Ext JS Senior Software Architect
    Personal Blog: http://edspencer.net
    Twitter: http://twitter.com/edspencer
    Github: http://github.com/edspencer

  3. #3
    Sencha User
    Join Date
    Jun 2007
    Posts
    46
    Vote Rating
    6
    kesteb is on a distinguished road

      0  

    Default


    Not publicly. When I figure out more of the state management stuff, I will put up a test site that can be viewed.

  4. #4
    Sencha - Community Support Team edspencer's Avatar
    Join Date
    Jan 2009
    Location
    Palo Alto, California
    Posts
    1,939
    Vote Rating
    7
    edspencer is a jewel in the rough edspencer is a jewel in the rough edspencer is a jewel in the rough

      0  

    Default


    Looking forward to it
    Ext JS Senior Software Architect
    Personal Blog: http://edspencer.net
    Twitter: http://twitter.com/edspencer
    Github: http://github.com/edspencer

  5. #5
    Sencha Premium Member
    Join Date
    Apr 2011
    Posts
    101
    Vote Rating
    3
    sskow200 is on a distinguished road

      1  

    Default Good work, a few comments.

    Good work, a few comments.


    First off, this is an excellent solution to a common problem. Can't wait to see it get added to the library. However, I have a few caveats of doing things this way. I have an app where I have many upon many components within it. In this case, on page render, all the components fire off a mess of state events all at once (sometimes 100's). I thought setting stateful: false would fix this issue, except then I realized that components that I didn't even create were firing state events. I want full control of my StateManager and it seemed I did not have it. Digging into the code, I found that by overriding the stateful configuration and setting stateful: false at this component level solved my problem. This got rid of all the components saving state that I did not care about.

    Code:
        Ext.override(Ext.AbstractComponent, { 
            stateful: false;
        });
    Returning to my page, the trivial components stopped firing events! Which then brought me to the panels I had selected to have state. They still seemed to fire alot of state saving events that I was not intending them to do so. Most of the time, these events are trivial (in my case anyway). For instance, I have a portal panel that has many portlets in it. When these components render, some of them fire off multiple 'resize' events. By the time the page has been loaded, this state save has been applied multiple times which means multiple ajax requests per component (10 components x 3 requests === too many requests). In my case, my components are all set width, so having the 'resize' event as a trigger is no use for me anyway. However, looking into the code base, you can see Ext.AbstractComponent automatically sets the 'resize' event in the constructor. Here, I override Ext.AbstractComponent again to something of this effect ...

    Code:
    Ext.override(Ext.AbstractComponent, {
         stateful: false,
         //have whatever events you wish to attach state to (in my case, none)
        defaultStateEvents: [],
        
        constructor: function(config){
            //omit constructor code
            //removed this.addStateEvents('resize') and changed to... 
            this.addStateEvents(this.defaultStateEvents);
            //continue omitted constructor     
        }
    });
    Voila! No more 'resize' event triggering my state manager. Drastic improvement on my requests! This brought me to my next find. On a different panel, I have a border layout with many components in it. This was firing off dozens of requests on page load as well. Digging into the 'Ext.layout.container.Border' component, I could see inside the function setupState(), again, automatically setting up state events to all child components without my knowledge! Same setup as with AbstractComponent, I overrode this component to something of this effect...

    Code:
    Ext.override(Ext.layout.container.Border, {
        
        //default state events for this component layout
        defaultStateEvents: [], 
    
        setupState: function(comp){
            var getState = comp.getState;
        
            comp.getState = function(){
    
                var state = getState.call(comp) || {}, 
                region = comp.region;
    
                state.collapsed = !!comp.collapsed;
                if (region == 'west' || region == 'east') {
                    state.width = comp.getWidth();
                } else {
                    state.height = comp.getHeight();
                }
    
                return state;
            };
    
            //removed comp.addStateEvents(['collapse', 'expand', 'resize']);
            comp.addStateEvents(this.defaultStateEvents);
        }   
    });
    Obviously, this is a case by case basis and not everyone will want to remove such events. However, I think what I was getting at here was a little more control of the component states. Also, if a component makes a change, why send off 1 requests per event on the same component? Seems to me that one state save for that component would be enough. Looks like there should be a queue in order here before we just start firing off a dozen events. Perhaps, this is the reason RemoteStorageProvider was not put into the original library. Even still, this same affect is happening with CookieProvider, but the results aren't as devastating because changing cookies is low-profile in terms of memory.

    In either case, job well done to you for creating this class and I love working in Ext4 as a whole. Just thought I would share some of my experience with the State Management. Thanks

  6. #6
    Sencha User
    Join Date
    Jun 2007
    Posts
    46
    Vote Rating
    6
    kesteb is on a distinguished road

      1  

    Default


    I have noticed the same behavior. From the documentation, only components that has "stateful" set "true" and a stateId should be saving there state. But in reality it seems that as soon as you provide a state provider, all components sorta save there state, and they sorta update themselves from the saved state. When you provide a "stateId' they do a better job of updating themselves. But enough on state management.

    The question would be where should this "queue" be managed. Within this class or on the Store. You can always set a delayed task to fire off every so many millisecond and check to see if there is any pending changes to the store and then sync the store. I have tried this and it does work. This is similar to what saki's HttpProvider does. But it is not an elegant solution.

    I would suggest that the Store should have a configuration option on how many changes should queue up before any one of the actions (create, update, delete) should be sent off to update the back end. In any case when multiple actions do happen at once the store does create an array of updates to send to the back end. For example here is the back end that handles my needs. It is written in Perl5.

    PHP Code:
    package Desktop::Controller::Desktop::State;

    our $VERSION '0.01';

    use Try::
    Tiny;
    use 
    XAS::Uaf::User;
    use 
    Desktop::Model::Database 'State';
    use 
    XAS::Class
      
    version   => $VERSION,
      
    base      => 'Desktop::Controller',
      
    codec     => 'JSON',
      
    constants => 'TRUE FALSE ARRAY HASH',
      
    mixin     => 'Desktop::Uaf::Authenticate XAS::Lib::ExtJS',
    ;

    use 
    Data::Dumper;

    # ----------------------------------------------------------------------
    # Public Methods
    # ----------------------------------------------------------------------

    sub do_read {
        
    my $self shift;

        
    my @data;
        
    my @rows;
        
    my $json;
        
    my $count 0;
        
    my $schema $self->scaffold->database->{desktop};
        
    my $params $self->scaffold->request->parameters;
        
    my $uaf_user $self->scaffold->session->get('uaf_user');
        
    my $user XAS::Uaf::User->new(-data => $uaf_user);
        
    my $criteria = {
            
    user_id => $user->attribute('user_id')
        };

        try {

            @
    rows State->search($schema$criteria);

            foreach 
    my $row (@rows) {

                
    $count++;

                
    my $datum = {
                    
    id    => $row->id,
                    
    name  => $row->name,
                    
    value => $row->value
                
    };

                
    push(@data$datum);

            }

            
    $json $self->format_data(
                -
    count => $count,
                -
    data  => \@data
            
    );

        } catch {

            
    my $ex $_;

            die 
    $ex;

        };

        
    $self->send_json($json);

    }

    sub do_create {
        
    my $self shift;

        
    my @data;
        
    my $json;
        
    my $count 0;
        
    my $schema $self->scaffold->database->{desktop};
        
    my $params decode($self->scaffold->request->parameters->get('data'));
        
    my $uaf_user $self->scaffold->session->get('uaf_user');
        
    my $user XAS::Uaf::User->new(-data => $uaf_user);

        try {

            if (
    ref($paramseq ARRAY) {

                foreach 
    my $record (@$params) {

                    
    my $datum $self->_create_state($schema$record$user);
                    if (
    defined($datum)) {

                        
    push(@data$datum);
                        
    $count++;

                    }

                }

            } else {

                
    my $datum $self->_create_state($schema$params$user);
                if (
    defined($datum)) {

                    
    push(@data$datum);
                    
    $count++;

                }
            }

            
    $json $self->format_data(
                -
    count => $count,
                -
    data  => \@data
            
    );

        } catch {

            
    my $ex $_;

            die 
    $ex;

        };

        
    $self->send_json($json);

    }

    sub do_update {
        
    my $self shift;

        
    my @data;
        
    my $json;
        
    my $count 0;
        
    my $schema $self->scaffold->database->{desktop};
        
    my $uaf_user $self->scaffold->session->get('uaf_user');
        
    my $user XAS::Uaf::User->new(-data => $uaf_user);
        
    my $params decode($self->scaffold->request->parameters->get('data'));

        try {

            if (
    ref($paramseq ARRAY) {

                foreach 
    my $record (@$params) {

                    
    my $datum $self->_update_state($schema$record$user);
                    if (
    defined($datum)) {

                        
    push(@data$datum);
                        
    $count++;

                    }

                }

            } else {

                
    my $datum $self->_update_state($schema$params$user);
                if (
    defined($datum)) {

                    
    push(@data$datum);
                    
    $count++;

                }

            }

            
    $json $self->format_data(
                -
    count => $count,
                -
    data  => \@data
            
    );

        } catch {

            
    my $ex $_;

            die 
    $ex;

        };

        
    $self->send_json($json);

    }

    sub do_delete {
        
    my $self shift;

        
    my @data;
        
    my $json;
        
    my $count 0;
        
    my $schema $self->scaffold->database->{desktop};
        
    my $uaf_user $self->scaffold->session->get('uaf_user');
        
    my $user XAS::Uaf::User->new(-data => $uaf_user);
        
    my $params decode($self->scaffold->request->parameters->get('data'));

        try {

            if (
    ref($paramseq ARRAY) {

                foreach 
    my $record (@$params) {

                    
    my $datum $self->_delete_state($schema$record$user);
                    if (
    defined($datum)) {

                        
    push(@data$datum);
                        
    $count++;

                    }

                }

            } else {

                
    my $datum $self->_delete_state($schema$params$user);
                if (
    defined($datum)) {

                    
    push(@data$datum);
                    
    $count++;

                }

            }

            
    $json $self->format_data(
                -
    count => $count,
                -
    data  => \@data
            
    );

        } catch {

            
    my $ex $_;

            die 
    $ex;

        };

        
    $self->send_json($json);

    }

    # ----------------------------------------------------------------------
    # Private Methods
    # ----------------------------------------------------------------------

    sub _create_state {
        
    my ($self$schema$record$user) = @_;

        
    my $data undef;
        
    my $rec = {
            
    user_id => $user->attribute('user_id'),
            
    name    => $record->{name},
            
    value   => $record->{value}
        };

        
    $schema->txn_do(sub {

            
    my $row State->update_or_create($schema$rec);

            
    $data = {
                
    id    => $row->id,
                
    name  => $row->name,
                
    value => $row->value
            
    };

        });

        return 
    $data;

    }

    sub _delete_state {
        
    my ($self$schema$record$user) = @_;

        
    my $data undef;

        if (
    my $row State->find($schema, { id => $record->{id} })) {

            
    $schema->txn_do(sub{

                
    $row->delete;
                
    $data = {
                    
    id    => $record->{id},
                    
    name  => $record->{name},
                    
    value => $record->{value}
                };

            });

        }

        return 
    $data;

    }

    sub _update_state {
        
    my ($self$schema$record$user) = @_;

        
    my $data undef;
        
    my $criteria = {
            
    user_id => $user->attribute('user_id'),
            
    name    => $record->{name}
        };

        if (
    my $row State->find($schema$criteria)) {

            
    $schema->txn_do(sub{

                
    $row->value($record->{value});
                
    $row->update;

                
    $data = {
                    
    id    => $row->id,
                    
    name  => $row->name,
                    
    value => $row->value
                
    };

            });

        }

        return 
    $data;

    }

    1;

    __END__

    =head1 NAME

    Desktop
    ::Controller::Desktop::State The controller to handle the desktop state.

    =
    head1 SYNOPSIS

    =head1 DESCRIPTION

    =head1 METHODS

    =over 4

    =item do_read

    Returns the state configuration 
    for the current user.

    =
    item do_create

    Creates a state item 
    for the current user.

    =
    item do_update

    Updates a state item
    .

    =
    item do_delete

    Deletes a state item
    .

    =
    back

    =head1 SEE ALSO

    =head1 AUTHOR

    Kevin L
    EstebE<lt>kevin@kesteb.usE<gt>

    =
    head1 COPYRIGHT AND LICENSE

    Copyright 
    (C2011 by Kevin LEsteb

    This library is free software
    you can redistribute it and/or modify
    it under the same terms 
    as Perl itselfeither Perl version 5.8.5 or,
    at your optionany later version of Perl 5 you may have available.

    =
    cut 
    This module will handle a single data request or multiple data requests. It also handles the funny issue where a create on a state item that is not really a create but an update. As I said the state stuff sorta works most of the time.

  7. #7
    Sencha User
    Join Date
    Jun 2007
    Posts
    46
    Vote Rating
    6
    kesteb is on a distinguished road

      0  

    Default


    Quote Originally Posted by edspencer View Post
    Gorgeous. Do you have examples of it running?
    I do now, http://test.kesteb.us it doesn't contain on of the above fixes.

  8. #8
    Sencha User
    Join Date
    Jun 2007
    Posts
    46
    Vote Rating
    6
    kesteb is on a distinguished road

      1  

    Default


    A minor update. I had to block in the constructor in order to make sure the store loads. Otherwise the first query is against an empty store.

    PHP Code:
    /*
     * File: RemoteStorageProvider.js
     * Date: 20-Jul-2011
     * By  : Kevin L. Esteb
     *
     * This module provides a state provider that uses remote storage as
     * the backing store.
     *
     *   RemoteStorageProvider.js is free software: you can redistribute
     *   it and/or modify it under the terms of the GNU General Public License
     *   as published by the Free Software Foundation, either version 3 of the
     *   License, or (at your option) any later version.
     *
     *   RemoteStorageProvider.js is distributed in the hope that it will be
     *   useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     *   General Public License for more details.
     *
     *   You should have received a copy of the GNU General Public License
     *   along with RemoteStorageProvider.js. If not, see
     *   <http://www.gnu.org/licenses/>.
     *
     */

    Ext.define('Ext.ux.state.RemoteStorageProvider', {
        
    extend'Ext.state.Provider',
        
    mixins: {
            
    observable'Ext.util.Observable'
        
    },
        
    uses: [
            
    'Ext.data.Model',
            
    'Ext.data.Store',
            
    'Ext.state.Provider',
            
    'Ext.util.Observable'
        
    ],

        
    store: {},

        
    constructor: function(config) {

           
    config config || {};
           
    this.initialConfig config;

           
    Ext.apply(thisconfig);

           
    this.store this.storeage();

           
    // Have to block to load the store before leaving the constructor
           // otherwise, the first query will be against an empty store.
           // There must be a better way...

           
    Ext.data.Connection.prototype.async false;
           
    this.store.load();
           
    Ext.data.Connection.prototype.async true;

           
    this.mixins.observable.constructor.call(thisconfig);

        },

        
    set: function(namevalue) {
            var 
    posrow;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    row this.store.getAt(pos);
                
    row.set('value'this.encodeValue(value));

            } else {

                
    this.store.add({
                    
    namename,
                    
    valuethis.encodeValue(value)
                });

            }

            
    this.store.sync();
            
    this.fireEvent('statechange'thisnamevalue);

        },

        
    get: function(namedefaultValue) {
            var 
    posrowvalue;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    row this.store.getAt(pos);
                
    value this.decodeValue(row.get('value'));

            } else {

                
    value defaultValue;

            }

            return 
    value;

        },

        
    clear: function(name) {
            var 
    pos;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    this.store.removeAt(pos);
                
    this.store.sync();
                
    this.fireEvent('statechange'thisnamenull);

            }

        },

        
    storeageExt.emptyFn

    }); 

  9. #9
    Sencha User
    Join Date
    Jun 2007
    Posts
    46
    Vote Rating
    6
    kesteb is on a distinguished road

      1  

    Default


    Another minor update. This one introduces the concept of throttling. It is turned off by default. Throttling will buffer state changes until a certain number have occurred, when that happens the changes are then batched to the store. You do lose some granularity in the state of the state. So you need to balance that out with performance needs.

    PHP Code:
    /*
     * File: RemoteStorageProvider.js
     * Date: 20-Jul-2011
     * By  : Kevin L. Esteb
     *
     * This module provides a state provider that uses remote storage as
     * the backing store.
     *
     *   RemoteStorageProvider.js is free software: you can redistribute
     *   it and/or modify it under the terms of the GNU General Public License
     *   as published by the Free Software Foundation, either version 3 of the
     *   License, or (at your option) any later version.
     *
     *   RemoteStorageProvider.js is distributed in the hope that it will be
     *   useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     *   General Public License for more details.
     *
     *   You should have received a copy of the GNU General Public License
     *   along with RemoteStorageProvider.js. If not, see
     *   <http://www.gnu.org/licenses/>.
     *
     */

    Ext.define('Ext.ux.state.RemoteStorageProvider', {
        
    extend'Ext.state.Provider',
        
    mixins: {
            
    observable'Ext.util.Observable'
        
    },
        
    uses: [
            
    'Ext.data.Model',
            
    'Ext.data.Store',
            
    'Ext.state.Provider',
            
    'Ext.util.Observable'
        
    ],

        
    store: {},
        
    throttledfalse,
        
    queue10,
        
    count0,

        
    constructor: function(config) {

           
    config config || {};
           
    this.initialConfig config;

           
    Ext.apply(thisconfig);

           
    this.store this.storeage();

           
    // Have to block in order to load the store before leaving the
           // constructor, otherwise, the first query may be against an
           // empty store. There must be a better way...

           
    Ext.data.Connection.prototype.async false;

           
    this.store.load();

           
    Ext.data.Connection.prototype.async true;

           
    this.mixins.observable.constructor.call(thisconfig);

        },

        
    set: function(namevalue) {
            var 
    posrow;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    row this.store.getAt(pos);
                
    row.set('value'this.encodeValue(value));

            } else {

                
    this.store.add({
                    
    namename,
                    
    valuethis.encodeValue(value)
                });

            }

            
    this.sync();
            
    this.fireEvent('statechange'thisnamevalue);

        },

        
    get: function(namedefaultValue) {
            var 
    posrowvalue;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    row this.store.getAt(pos);
                
    value this.decodeValue(row.get('value'));

            } else {

                
    value defaultValue;

            }

            return 
    value;

        },

        
    clear: function(name) {
            var 
    pos;

            if ((
    pos this.store.find('name'name)) > -1) {

                
    this.store.removeAt(pos);
                
    this.sync();
                
    this.fireEvent('statechange'thisnamenull);

            }

        },

        
    sync: function() {

            if (
    this.throttled) {

                if (
    this.count >= this.queue) {

                    
    this.count 0;
                    
    this.store.sync();

                } else {

                    
    this.count++;

                }

            } else {

                
    this.store.sync();

            }

        },

        
    storeageExt.emptyFn

    }); 

  10. #10
    Sencha User Vital Aaron's Avatar
    Join Date
    Jun 2011
    Posts
    24
    Vote Rating
    0
    Vital Aaron is on a distinguished road

      0  

    Default One glitch...

    One glitch...


    The function 'storeage' should be spelled 'storage'.

Turkiyenin en sevilen filmlerinin yer aldigi xnxx internet sitemiz olan ve porn sex tarzi bir site olan mobil porno izle sitemiz gercekten dillere destan bir durumda herkesin sevdigi bir site olarak tarihe gececege benziyor. Sitenin en belirgin ozelliklerinden birisi de Turkiyede gercekten kaliteli ve muntazam, duzenli porno izle siteleri olmamasidir. Bu yuzden iste. Ayrica en net goruntu kalitesine sahip adresinde yayinlanmaktadir. Mesela diğer sitelerimizden bahsedecek olursak, en iyi hd porno video arşivine sahip bir siteyiz. "The Best anal porn videos and slut anus, big asses movies set..." hd porno faketaxi