Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

puzzled about local storage adapter behaviour #86

Open
st-h opened this issue Oct 29, 2015 · 3 comments
Open

puzzled about local storage adapter behaviour #86

st-h opened this issue Oct 29, 2015 · 3 comments

Comments

@st-h
Copy link
Contributor

st-h commented Oct 29, 2015

I have created a few test cases to find out how the local storage adapter works:
I also tried creating a twiddle, but failed with a Namespace error in ember-graph, so here are the classes:

adapters/persistent-token.js:

export default EG.LocalStorageAdapter.extend({

});

models/persistent-token.js:

export default EG.Model.extend({
  token: EG.attr({ type: 'string' })
});

If I call in an action in route/application.js:

this.get('store').pushPayload({persistentToken: [{id: 'testPush', token: 'test1'}]})

and then:

this.get('store').find('persistentToken', 'testPush').then(function(token) {
        if (!token) {
          console.log('no token');
          return;
        }
        console.log('token: ' +  token + ' id: ' + token.get('id') + ' val: ' + token.get('token'));
      }, function(error) {
        console.error('failed to find token: ' + JSON.stringify(error))
      })

The token is retrieved as expected

token: <testapp@model:persistent-token::ember758> id: testPush val: test1

If I reload the page and call the above function to retrieve the token again, I receive the following error:

failed to find token: {"status":404,"typeKey":"persistentToken","id":"testPush"}

I do not see any entry in chrome's local storage resource. And the data is lost after a reload of the page

Trying to use the save method to persist the token:

      var version = this.get('store').createRecord('persistentToken', {
        id: 'testSave',
        token: 'test2'
      });
      version.save();

and retrieving it with a similar function:

      this.get('store').find('persistentToken', 'testSave').then(function(token) {
        if (!token) {
          console.log('no token');
          return;
        }
        console.log('token: ' +  token + ' id: ' + token.get('id') + ' val: ' + token.get('token'));
      }, function(error) {
        console.error('failed to find token: ' + JSON.stringify(error))
      })

The token is not found. Output is:

failed to find token: {"status":404,"typeKey":"persistentToken","id":"testSave"}

However, an entry to the local storage resource has been added. A find all shows that the id has been automatically assigned, even though one had been provided:

this.get('store').find('persistentToken').then(function(token) {
        if (!token) {
          console.log('no token');
          return;
        }
        token.forEach(function(t) {
          console.log('token: ' +  t + ' id: ' + t.get('id') + ' val: ' + t.get('token'));
        })
      }, function(error) {
        console.error('failed to find token: ' + JSON.stringify(error))
      })

leads to output:

token: <testapp@model:persistent-token::ember774> id: 1143a7b2-9334-4480-8ab0-01ee8d1d798d val: test2

If the page is reloaded, the token still is there. However, it is difficult to remember an automatically assigned id between reloads and I think this mainly defeats the purpose.

Am I doing anything wrong, or are these bugs?

@gordonkristan
Copy link
Contributor

This is actually expected behavior (although possibly not desirable behavior). To explain your two scenarios:

  1. When using pushPayload(), your model isn't persisted across page refreshes. This is because you never actually save your model to the local storage, you just load it into the store (which is just in memory). Persisting your model would normally require a model.save() call. But even if you did that it wouldn't persist it to the localStorage because it doesn't see any changes in the model. When you push a model with an ID into the store, Ember Graph assumes that the model is persisted somewhere already, and it's in a clean state. It's not going to attempt to save it again because it doesn't think there's anything to save. (Remember that most use cases require an AJAX request to save, there's no reason to do extraneous ones.)
  2. Your new model created by createRecord() is saved to the localStorage but not with the right ID. This is actually expected as well. createRecord() doesn't accept an ID because it's creating a new record. I assumed (perhaps stupidly) that IDs are only created by the server, not the client. So the record is created in the store without an ID, then when you call save() it's sent to the adapter for persistence, which gives it an ID. If you want to give it a custom ID, feel free to override the generateIdForRecord hook in your adapter.

Finally, I'd like to point out that the workflow you seem to want to use may not be the right one. Remember that the LocalStorageAdapter can be used for testing and mocking data, but it's also good for actually storing real data in the local storage, which is why it functions more like a data store than an adapter. If you find that you just want to load mock data for testing purposes, you'll be better off overriding shouldInitializeDatabase and getInitialPayload.

export default EmberGraph.LocalStorageAdapter.extend({
    shouldInitializeDatabase() {
        return true;
    },

    getInitialPayload() {
        return {
            persistentToken: [
                { id: 'testPush', token: 'test1' }
            ]
        };
    }
});

That code will load the payload returned by getInitialPayload() into local storage every time your page refreshes. (You can also make the logic for shouldInitializeDatabase() smarter if you need.) This might be more of what you're looking for.

@st-h
Copy link
Contributor Author

st-h commented Oct 29, 2015

Thanks for the explanation. I see that pushPayload does not really make sense when dealing with local persistence, as the data is just pushed to the store and not the adapter. However, that sounds like there will be a lot of issues for me on the way. I plan on having quite some data that might just be pushed from the server to the client. So there will never be an ajax request or similar - just data from the server. I need to be able to explicitly tell the storage to just save that record, without doing any extras like assigning its own ids. There is another pattern that is quite common, which are PUT requests. They usually allow to create records with a specific id on the server. In that case the id isn't assigned on the server as well, but provided by the client.
Anyway. Probably using the LocalStorageAdapter is not the right way to go for me at all. I will have use cases where the client will request data from the server (through the store/adapter) and the retrieved data should be persisted to local storage as well. So using two adapters probably isn't not the best idea. Is there any hook I could use when data is persisted in to the store (not local changes, but changes that where acknowledged by the server), that I could use to trigger my own implementation which handles local storage? Ah well.. probably the adapter would be a good point for such a thing, after the data has been deserialised.

@gordonkristan
Copy link
Contributor

I will have use cases where the client will request data from the server (through the store/adapter) and the retrieved data should be persisted to local storage as well.

Are you just looking for a caching layer for your server data? If so there's a few ways you could implement that. One would be to override the pushPayload() method. Then just re-load that data when your application loads.

EmberGraph.Store.extend({
    pushPayload(payload, cache = true) {
        if (cache) {
            // You might need a serializer here if it's not JSON...
            localStorage[`cached_payload_${Date.now()}`] = JSON.stringify(payload);
        }

        this._super(payload);
    }
});

Ember.Application.extend({
    ready() {
        const store = this.get('store');
        const cachedDataKeys = Object.keys(localStorage).filter((key) => key.startsWith('cached_payload_'));

        cachedDataKeys.forEach((key) => {
            store.pushPayload(JSON.parse(localStorage[key]), false);
            delete localStorage[key];
        });
    }
});

You could always perform the caching in your adapter as well (which would be preferable if possible). I've done something like this before (simplified example).

EmberGraph.RESTAdapter.extend({
    findRecord(typeKey, id) {
        const cacheKey = `record:${typeKey}:${id}`;
        if (localStorage[cacheKey]) {
            const cacheValue = JSON.parse(localStorage[cacheKey]);

            if (cacheValue.timestamp + CACHE_EXPIRATION > Date.now()) {
                return Promise.resolve(cacheValue.payload);
            }
        }

        const url = this.buildUrl(typeKey, id);
        return this.ajax(url, 'GET').then((payload) => {
            localStorage[cacheKey] = JSON.stringify({
                timestamp: Date.now(),
                payload
            });

            return this.deserialize(payload, { requestType: 'findRecord', recordType: typeKey, id });
        });
    }
});

Hopefully that makes sense? If not let me know. I've done stuff like this before with both Ember Graph and Ember Data, so I'm sure there's a way to do what you need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants