FatFractal customer forums



Author Topic: Recent release 1.2 - introducing client-side caching  (Read 2648 times)

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Recent release 1.2 - introducing client-side caching
« on: January 16, 2014, 01:30:13 PM »
As of last weekend's release, the iOS and Android SDKs now provide client-side caching support (for offline behaviours).

I am preparing a blog post about this feature, but I was ill for a couple of days and am still playing catch-up, so thought I'd better post this :-)

Latest downloads are at http://fatfractal.com/support/downloads/

The example below is for IOS but the Android SDK works in pretty much exactly the same way. It's pretty straightforward to use; here's a little test harness which
  • sets up local storage
  • logs in
  • creates some objects
  • Retrieves some objects using a query, & store to cache
  • simulates being offline
  • Issue the query but don't ask to use the cache - should be an error
  • Issue the same query again, asking to use the cache if offline - should work fine

Code: [Select]
#import <SenTestingKit/SenTestingKit.h>
#import <FFEF/FatFractal.h>

static FatFractal * ff;
static id<FFLocalStorage> localStorage;

@interface Basic : NSObject
@property (nonatomic) int anInt;
@end

@implementation Basic
@end

@interface BasicCachingTest : SenTestCase

@end

@implementation BasicCachingTest

- (void)setUp
{
    [super setUp];
   
    if (! ff) {
        ff = [[FatFractal alloc] initWithBaseUrl:@"https://localhost:8443/TestApp"];
        // Do any calls to registerClass:ForClazz: here, BEFORE setting the localStorage property
        // This is essential when one is subclassing FFUser for example

        localStorage = [[FFLocalStorageSQLite alloc] initWithDatabaseKey:@"CachingTests"];
        [localStorage setDebug:NO];
        [localStorage wipeAllData];
       
        ff.localStorage = localStorage;
    }
    ff.debug = NO;
    ff.simulatingOffline = NO;
}

- (void)tearDown
{
    [super tearDown];
}

- (void) pause:(NSTimeInterval)pauseInterval
{
    NSDate* cycle = [NSDate dateWithTimeIntervalSinceNow:pauseInterval];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                             beforeDate:cycle];
}

- (NSError *) deleteObjsForQuery:(NSString *)query {
    NSError *crudError;
   
    NSArray * objs = [ff getArrayFromUri:query error:&crudError];
   
    if (! crudError) {
        for (id obj in objs) {
            crudError = nil;
            [ff deleteObj:obj error:&crudError];
            STAssertNil(crudError, @"deleteObjectsForQuery %@ : Failed to delete %@", query, obj);
            if (crudError)
                break;
        }
    }
    return crudError;
}

- (void) testBasicCreateAndRetrieve
{
    // We're going to run this all synchronously but use the block methods which would be used in normal code
    __block BOOL blockComplete = NO;
    __block NSError *blockErr;
   
    // Login
    NSLog(@"***testBasicCreateAndRetrieve: logging in");
    [ff loginWithUserName:@"TestUser_1" andPassword:@"TestUser_1" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
        STAssertNil(theErr, @"Failed to login - error was: %@", [theErr localizedDescription]);
        blockErr = theErr;
        blockComplete = YES;
    }];
    while (!blockComplete) [self pause:0.001];
    if (blockErr) return;
   
    // Delete existing test objects
    NSLog(@"***testBasicCreateAndRetrieve: deleting old test objects");
    NSError *deleteErr = [self deleteObjsForQuery:@"/Basic"];
    STAssertNil(deleteErr, @"Failed to delete existing objects");
    if (deleteErr) return;
   
    // Create 5 objects
    NSLog(@"***testBasicCreateAndRetrieve: creating new test objects");
    for (int i = 1; i <= 5; i++) {
        blockErr = nil;
        blockComplete = NO;
        Basic * basic = [[Basic alloc] init]; basic.anInt = i;
        [ff createObj:basic atUri:@"/Basic" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
            blockErr = theErr;
            STAssertNil(blockErr, @"Failed to create object - error was: %@", [blockErr localizedDescription]);
            blockComplete = YES;
        }];
        while (!blockComplete) [self pause:0.001];
        if (blockErr) return;
    }
   
    // Retrieve 2 of the objects via a query, and ask for the response to be cached
    NSLog(@"***testBasicCreateAndRetrieve: retrieving objects - caching response");
    blockErr = nil;
    blockComplete = NO;
    [[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithOptions:FFReadOptionCacheResponse andBlock:^(FFReadResponse *response)
     {
         blockErr = [response error];
         STAssertNil(blockErr, @"Failed to retrieve objects - error was: %@", [blockErr localizedDescription]);
         blockComplete = YES;
     }];
    while (!blockComplete) [self pause:0.001];
    if (blockErr) return;
   
    // Simulate being offline
    NSLog(@"***testBasicCreateAndRetrieve: simulating offline");
    [ff setSimulatingOffline:YES];
   
    // Issue the query again but don't ask to use the cache - should be an error
    NSLog(@"***testBasicCreateAndRetrieve: retrieving objects while offline - NOT USING cache - should get an error response");
    blockErr = nil;
    blockComplete = NO;
    [[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithBlock:^(FFReadResponse *response)
     {
         blockErr = [response error];
         STAssertNotNil(blockErr, @"Should have failed to retrieve objects");
         blockComplete = YES;
     }];
    while (!blockComplete) [self pause:0.001];
    if (! blockErr) return;
   
    // Issue the same query again, asking to use the cache if offline - should work fine
    NSLog(@"***testBasicCreateAndRetrieve: retrieving objects while offline - USING cache");
    blockErr = nil;
    blockComplete = NO;
    [[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithOptions:FFReadOptionUseCachedIfOffline andBlock:^(FFReadResponse *response)
     {
         blockErr = [response error];
         STAssertNil(blockErr, @"Failed to retrieve objects from cache - error was: %@", [blockErr localizedDescription]);
         blockComplete = YES;
     }];
    while (!blockComplete) [self pause:0.001];
    if (blockErr) return;
}

@end

FYI, the various read options which you can use (and of course you can use some of them together) are:
Code: [Select]
typedef NS_OPTIONS(NSInteger, FFReadOption) {
    FFReadOptionAutoLoadRefs        = (0x1 << 0), // When retrieving objects, automatically retrieve objects which they reference
    FFReadOptionAutoLoadBlobs       = (0x1 << 1), // When retrieving objects, automatically retrieve any BLOBs they contain
    FFReadOptionCacheResponse       = (0x1 << 2), // When retrieving, cache the response
    FFReadOptionUseCachedOnly       = (0x1 << 3), // When retrieving, ONLY try the cache - i.e. do not hit the network
    FFReadOptionUseCachedIfCached   = (0x1 << 4), // When retrieving, try the cache first - only hit the network if cache is empty
    FFReadOptionUseCachedIfOffline  = (0x1 << 5)  // When retrieving, try the network first - if offline, then try the cache
};

Personally I tend to use FFReadOptionCacheResponse | FFReadOptionUseCachedIfOffline for all of the queries that I wish to have available while offline.

One nice additional benefit - when using localStorage like this, the logged-in user is also cached - so if for example you log in and then the app is terminated, then, when the app restarts and the FatFractal object is created again, it will restore the logged-in user, and loggedIn will be true.

Give it a try, and let us know what you think!

- Gary

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
CoreData (Was: Re: Recent release 1.2 - introducing client-side caching)
« Reply #1 on: January 18, 2014, 06:11:15 AM »
I should mention that because another customer needed it, we've made some tweaks to the iOS SDK in the past few days which means it now more comfortably handles CRUD for CoreData objects. There's no deep integration for now - but the basic integration between CoreData's dynamic properties & entity model and FatFractal's seamless serialization / deserialization is done.

If you're interested in an "early access" release of the iOS SDK with these tweaks in place, please let me know. And if you are keen to use FatFractal but are waiting for further CoreData integration, post a message here in the forums, let us know what you need.

- Gary
« Last Edit: January 18, 2014, 06:28:05 AM by gkc »

kwylez

  • Newbie
  • *
  • Posts: 27
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #2 on: January 23, 2014, 04:00:21 PM »
Gary what are the benefits of using
Code: [Select]
[ff newReadRequest] prepareGetFromUri... versus
Code: [Select]
getArrayFromUri:onComplete?

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #3 on: January 23, 2014, 04:05:50 PM »
The main thing is that when you then call [readRequest executeAsyncWithOptions:andBlock:] you can pass a number of options which determine how the caching will behave

kwylez

  • Newbie
  • *
  • Posts: 27
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #4 on: January 23, 2014, 04:13:24 PM »
That's great Gary. Custom caching for requests. Love it.

Ken

  • Newbie
  • *
  • Posts: 18
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #5 on: March 16, 2014, 09:24:53 PM »
Awesome stuff!

Thanks  for the detailed explanation Gary!

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #6 on: March 17, 2014, 08:49:46 AM »
@vkenchin glad you like it! all feedback welcome, it's a relatively new feature but is fundamental to one of our high-traffic apps (several million API requests per day peak) where it works like a champ

ssdscott

  • Newbie
  • *
  • Posts: 3
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #7 on: April 22, 2014, 12:44:09 AM »
Hi - Stackmob refugee here, evaluating FatFractal.  local caching is important to my current project.  I built iOS first and was happy with SM; when I switched to Android, not so much… so I probably would have left anyways, though perhaps not so soon.  since caching is so new to FF i'm a bit nervous.  here's what i'm after:  When user is connected, and the app runs queries, the results are stored locally - as an object graph.  (on android, separate SM queries for the same object result in different objects  :( ).  When the user is disconnected, any changes to existing objects, or new objects, are stored locally for later update.  (whether this is an explicit update that the app has to do, or more like Parse's "saveEventually" is less important - as long as it's clear which is going on! )
TIA.

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #8 on: April 23, 2014, 05:50:26 AM »
@ssdscott - with regards to storing objects locally you have two options (they can co-exist of course) really
(1) Use CoreData or similar mechanism - see here for an example https://github.com/FatFractal/fatfractal-code-samples/tree/master/TaggedLocations and here for further discussion https://forum.fatfractal.com/forum/index.php?topic=111.msg635#msg635

(2) Use the FF caching. In effect what this allows you to do is to make a request to the backend and have the full response stored locally; then if you make the request again, with UseCachedIfOffline, then you will get the cached response. This works for any GET requests.

With regards to offline - sure - use one of the queueCreate / queueUpdate / queueDelete family of methods; or alternatively, again, you can build a CoreData integration

Cheers,

- Gary

ssdscott

  • Newbie
  • *
  • Posts: 3
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #9 on: April 24, 2014, 10:12:46 AM »
Hi Gary -
Thanks for the note.  Is there a limit on the cache lifetime?  I stumbled on a Parse forum entry yesterday (which of course I can't find again) that mentioned a cache size limit of 10K after which stuff starts to get tossed.

Also, is there an alternative to CoreData for Android?  Or are the only options FF caching or roll-your-own?


I have to make a decision here SOON between FF (i like the object graph support and language support for server-side coding) or Parse (survived an acquisition), so appreciate your promptness!

susan

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #10 on: April 24, 2014, 03:57:12 PM »
Hi Susan,

Re cache lifetime - right now there is no lifetime; there is a version in the works (should be releasable next week) which gives developer control over size of cache. Cache eviction policy will be LeastRecentlyUsed and you will have ability to remove individual items from the cache programatically

There are quite a lot of other StackMob refugees who have chosen us - all getting on fine (and loving our support ethos!) - a few of them have posted here on the forums
« Last Edit: April 24, 2014, 04:00:30 PM by gkc »

dev

  • Newbie
  • *
  • Posts: 15
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #11 on: April 25, 2014, 05:32:21 AM »
Gary,
This is so awesome, thank you so much!

Is there any plans on implementing wipes for specific data? Instead of:
Code: [Select]
- (void) wipeAllData
Use something like:
Code: [Select]
- (void) wipeDataWithKey:(NSString*)
Cheers

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #12 on: April 28, 2014, 05:14:32 PM »
Yep that's right (that's what I meant by "remove individual items from the cache programatically")

dev

  • Newbie
  • *
  • Posts: 15
    • View Profile
Re: Recent release 1.2 - introducing client-side caching
« Reply #13 on: April 29, 2014, 07:12:32 AM »
Oh, missed that line. Great news!

 

Copyright © FatFractal customer forums