Skip to content

Commit c069e60

Browse files
committed
added soft expiration - via server revalidation
intermediate state, won't work property right now
1 parent f430811 commit c069e60

11 files changed

Lines changed: 259 additions & 50 deletions

File tree

‎JSONModel/JSONModelNetworking/JSONCache.h‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,37 @@
1515
// The MIT License in plain English: http://www.touch-code-magazine.com/JSONModel/MITLicense
1616

1717
#import <Foundation/Foundation.h>
18+
#import "JSONCacheFile.h"
19+
#import "JSONCacheResponse.h"
20+
21+
extern int kNeverRevalidate;
22+
extern int kAlwaysRevalidate;
23+
24+
extern int kNeverExpire;
25+
extern int kImmediatelyExpire;
1826

1927
@interface JSONCache : NSObject
2028

29+
@property (assign, nonatomic, readonly) BOOL isOnline;
30+
2131
@property (assign, nonatomic) int expirationTimeInHours;
2232
@property (assign, nonatomic) int expirationTimeInHoursWhenOffline;
2333

2434
@property (assign, nonatomic) BOOL isOfflineCacheEnabled;
25-
@property (assign, nonatomic, readonly) BOOL isOnline;
35+
36+
@property (assign, nonatomic) int revalidateCacheFromServerAfterTimeInHours;
37+
@property (assign, nonatomic) int revalidateCacheViaETagAfterTimeInHours;
38+
39+
@property (assign, nonatomic) BOOL isUsingXdHTTPHeaderNames;
2640

2741
+(instancetype)sharedCache;
2842

2943
-(BOOL)addObject:(id)object forMethod:(NSString*)method andParams:(id)params;
30-
-(id)objectForMethod:(NSString*)method andParams:(id)params;
44+
-(JSONCacheResponse*)objectForMethod:(NSString*)method andParams:(id)params;
3145

3246
-(void)trimExpiredObjects;
3347
-(void)trimObjectForKey:(NSString*)key;
3448

3549
-(void)purgeCache;
3650

3751
@end
38-
39-
#pragma mark - helper cache classes
40-
@interface JSONCacheFile : NSObject
41-
42-
@property (strong, nonatomic) NSString* name;
43-
@property (assign, nonatomic) NSTimeInterval modificationTime;
44-
45-
@end

‎JSONModel/JSONModelNetworking/JSONCache.m‎

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,22 @@
2222
#import <netinet/in.h>
2323
#import <netinet6/in6.h>
2424

25+
int kNeverRevalidate = INT_MAX;
26+
int kAlwaysRevalidate = 0;
27+
28+
int kNeverExpire = INT_MAX;
29+
int kImmediatelyExpire = 0;
30+
2531
#define kJSONCacheDirectory @"JSONCache"
2632
#define kDefaultExpirationTimeInHours 1 //1 day
2733
#define kDefaultExpirationTimeInHoursWhenOffline 168 //1 week
2834

35+
#define kIfModifiedSinceHTTPHeaderName @"If-Modified-Since"
36+
#define kETagHTTPHeaderName @"ETag"
37+
38+
#define kIfModifiedSinceHTTPHeaderNameXd @"X-If-Modified-Since"
39+
#define kETagHTTPHeaderNameXd @"X-ETag"
40+
2941
static JSONCache* instance = nil;
3042

3143
#pragma mark - cache implementation
@@ -40,6 +52,9 @@ @implementation JSONCache
4052
NSTimeInterval _expirationOnlineTimeInterval;
4153
NSTimeInterval _expirationOfflineTimeInterval;
4254

55+
NSTimeInterval _revalidateFromServerTimeInterval;
56+
NSTimeInterval _revalidateFromServerWithETagTimeInterval;
57+
4358
SCNetworkReachabilityRef reachabilityRef;
4459
BOOL observingConnection;
4560
}
@@ -68,6 +83,12 @@ -(id)_initPrivate
6883
self.expirationTimeInHours = kDefaultExpirationTimeInHours;
6984
self.expirationTimeInHoursWhenOffline = kDefaultExpirationTimeInHoursWhenOffline;
7085

86+
self.revalidateCacheFromServerAfterTimeInHours = kNeverRevalidate;
87+
self.revalidateCacheViaETagAfterTimeInHours = kNeverRevalidate;
88+
89+
self.isUsingXdHTTPHeaderNames = YES;
90+
_isOnline = YES;
91+
7192
//setup the directories
7293
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory , NSUserDomainMask, YES);
7394
cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:kJSONCacheDirectory];
@@ -88,12 +109,13 @@ -(id)_initPrivate
88109

89110
//read the current cache contents
90111
for (NSString* fileName in directoryContents) {
112+
JMLog(@"cache file: %@", fileName);
91113
NSString* fullFilePath = [cacheDirectory stringByAppendingPathComponent: fileName];
92114
NSDate* modifiedDate = [[fm attributesOfItemAtPath:fullFilePath error:NULL] fileModificationDate];
93115

94116
JSONCacheFile* file = [[JSONCacheFile alloc] init];
95117
file.modificationTime= [modifiedDate timeIntervalSinceReferenceDate];
96-
file.name = fileName;
118+
file.fileName = fileName;
97119

98120
[cacheFiles setValue:file forKey:fileName];
99121
}
@@ -105,14 +127,20 @@ -(id)_initPrivate
105127
if (cacheFiles.count>0 && observingConnection==NO) {
106128
//observe connectivity changes
107129
[self startConnectivityObserver];
108-
JMLog(@"JSONCache.isOnline = %@", _isOnline?@"YES":@"NO");
130+
JMLog(@"JSONCache.isOnline = %@", _isOnline?@"YES":@"NO" );
109131
}
110132
}
111133

112134
return self;
113135
}
114136

115137
-(BOOL)addObject:(id)object forMethod:(NSString*)method andParams:(id)params
138+
{
139+
return [self addObject:object forMethod:method andParams:params etag:nil];
140+
}
141+
142+
143+
-(BOOL)addObject:(id)object forMethod:(NSString*)method andParams:(id)params etag:(NSString*)etag
116144
{
117145
//expire objects
118146
[self trimExpiredObjects];
@@ -127,9 +155,7 @@ -(BOOL)addObject:(id)object forMethod:(NSString*)method andParams:(id)params
127155
BOOL success = [data writeToFile:filePath atomically:YES];
128156
if (success) {
129157
//save the cache file
130-
JSONCacheFile* file = [[JSONCacheFile alloc] init];
131-
file.name = key;
132-
file.modificationTime = [[NSDate date] timeIntervalSinceReferenceDate];
158+
JSONCacheFile* file = [[JSONCacheFile alloc] initWithKey:key andEtag:etag];
133159
[cacheFiles setValue: file forKey:key];
134160
}
135161

@@ -142,7 +168,7 @@ -(BOOL)addObject:(id)object forMethod:(NSString*)method andParams:(id)params
142168
return success;
143169
}
144170

145-
-(id)objectForMethod:(NSString*)method andParams:(id)params
171+
-(JSONCacheResponse*)objectForMethod:(NSString*)method andParams:(id)params
146172
{
147173
NSString* key = [self keyForMethod:method andParams:params];
148174
JSONCacheFile* file = cacheFiles[key];
@@ -157,8 +183,52 @@ -(id)objectForMethod:(NSString*)method andParams:(id)params
157183

158184
NSString* filePath = [cacheDirectory stringByAppendingPathComponent:key];
159185
NSData* data = [NSData dataWithContentsOfFile: filePath];
160-
id result = [NSKeyedUnarchiver unarchiveObjectWithData:data];
161-
return result;
186+
187+
id object;
188+
@try {
189+
object = [NSKeyedUnarchiver unarchiveObjectWithData:data];
190+
} @catch (NSException* e) {
191+
return nil;
192+
}
193+
194+
JSONCacheResponse* response = [[JSONCacheResponse alloc] init];
195+
response.object = object;
196+
response.isOfflineVersion = !self.isOnline;
197+
198+
if (self.isOnline && self.revalidateCacheFromServerAfterTimeInHours!=kNeverRevalidate) {
199+
200+
//check for revalidation rules
201+
NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate];
202+
NSTimeInterval expiration = _revalidateFromServerTimeInterval;
203+
if (now > file.modificationTime + expiration) {
204+
response.mustRevalidate = YES;
205+
206+
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
207+
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZ"];
208+
NSDate* lastModificationDate = [NSDate dateWithTimeIntervalSinceReferenceDate:file.modificationTime];
209+
210+
[response.revalidateHeaders setValue: [dateFormatter stringFromDate: lastModificationDate]
211+
forKey: self.isUsingXdHTTPHeaderNames? kIfModifiedSinceHTTPHeaderNameXd: kIfModifiedSinceHTTPHeaderName ];
212+
}
213+
}
214+
215+
if (self.isOnline && self.revalidateCacheViaETagAfterTimeInHours!=kNeverRevalidate) {
216+
//check for the revalidation rules
217+
NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate];
218+
NSTimeInterval expiration = now + _revalidateFromServerWithETagTimeInterval;
219+
220+
if (now > file.modificationTime + expiration) {
221+
response.mustRevalidate = YES;
222+
223+
if (file.etag != nil) {
224+
[response.revalidateHeaders setValue: file.etag
225+
forKey: self.isUsingXdHTTPHeaderNames? kETagHTTPHeaderNameXd: kETagHTTPHeaderName ];
226+
}
227+
}
228+
}
229+
230+
NSLog(@"CASHED RESULT!!!");
231+
return response;
162232
}
163233

164234
-(NSString*)keyForMethod:(NSString*)method andParams:(id)params
@@ -176,14 +246,15 @@ -(void)trimExpiredObjects
176246
{
177247
for (NSString* key in [cacheFiles allKeys]) {
178248
if ([self isObjectExpired: cacheFiles[key]]) {
179-
//expired content
249+
//expired content
180250
[self trimObjectForKey: key];
181251
}
182252
}
183253
}
184254

185255
-(void)trimObjectForKey:(NSString*)key
186256
{
257+
NSLog(@"delete file: %@", key);
187258
NSString* filePath = [cacheDirectory stringByAppendingPathComponent:key];
188259
[fm removeItemAtPath:filePath error:NULL];
189260
[cacheFiles removeObjectForKey: key];
@@ -286,22 +357,19 @@ -(BOOL)isObjectExpired:(JSONCacheFile*)object
286357
NSTimeInterval expiration = 0;
287358

288359
if (self.isOnline==NO && self.isOfflineCacheEnabled==NO) {
289-
360+
NSLog(@"expire %@ for Offline:offline disabled", object.key);
290361
//it's always expired
291362
return YES;
292363

293364
} else if (self.isOnline==YES && self.isOfflineCacheEnabled==NO) {
294-
295365
//online cache expiration
296366
expiration = _expirationOnlineTimeInterval;
297367

298368
} else if (self.isOnline==YES && self.isOfflineCacheEnabled==YES) {
299-
300369
//offline expiration time
301370
expiration = _expirationOfflineTimeInterval;
302371

303372
} else {
304-
305373
//just return the longer of the two exp. times
306374
expiration = MAX(_expirationOfflineTimeInterval, _expirationOnlineTimeInterval);
307375

@@ -342,12 +410,16 @@ -(void)setExpirationTimeInHoursWhenOffline:(int)expirationTimeInHoursWhenOffline
342410
_expirationOfflineTimeInterval = expirationTimeInHoursWhenOffline * 360;
343411
}
344412

345-
@end
413+
-(void)setRevalidateCacheFromServerAfterTimeInHours:(int)hrs
414+
{
415+
_revalidateCacheFromServerAfterTimeInHours = hrs;
416+
_revalidateFromServerTimeInterval = hrs * 360;
417+
}
346418

347-
#pragma mark - helper classes
348-
@implementation JSONCacheFile
349-
-(NSString*)description
419+
-(void)setRevalidateCacheViaETagAfterTimeInHours:(int)hrs
350420
{
351-
return self.name;
421+
_revalidateCacheViaETagAfterTimeInHours = hrs;
422+
_revalidateFromServerWithETagTimeInterval = hrs * 360;
352423
}
353-
@end
424+
425+
@end

‎JSONModel/JSONModelNetworking/JSONCacheFile.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,12 @@
1010

1111
@interface JSONCacheFile : NSObject
1212

13-
@end
13+
@property (strong, nonatomic) NSString* fileName;
14+
@property (assign, nonatomic) NSTimeInterval modificationTime;
15+
16+
@property (strong, nonatomic, readonly) NSString* key;
17+
@property (strong, nonatomic, readonly) NSString* etag;
18+
19+
-(instancetype)initWithKey:(NSString*)key andEtag:(NSString*)etag;
20+
21+
@end

‎JSONModel/JSONModelNetworking/JSONCacheFile.m‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,38 @@
1010

1111
@implementation JSONCacheFile
1212

13+
-(instancetype)initWithKey:(NSString*)key andEtag:(NSString*)etag
14+
{
15+
self = [super init];
16+
if (self!=nil) {
17+
if (etag!=nil) {
18+
self.fileName = [NSString stringWithFormat:@"%@~%@~etag",key,etag];
19+
} else {
20+
self.fileName = key;
21+
}
22+
self.modificationTime = [[NSDate date] timeIntervalSinceReferenceDate];
23+
}
24+
return self;
25+
}
26+
27+
-(void)setFileName:(NSString *)fileName
28+
{
29+
_fileName = fileName;
30+
if ([_fileName hasSuffix:@"etag"]) {
31+
//etag attached to object name
32+
NSArray* nameParts = [_fileName componentsSeparatedByString:@"~"];
33+
if (nameParts.count>2) {
34+
_etag = nameParts[nameParts.count-2];
35+
_key = nameParts[nameParts.count-3];
36+
}
37+
} else {
38+
_key = fileName;
39+
}
40+
}
41+
42+
-(NSString*)description
43+
{
44+
return self.fileName;
45+
}
46+
1347
@end

‎JSONModel/JSONModelNetworking/JSONCacheResponse.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@
1010

1111
@interface JSONCacheResponse : NSObject
1212

13+
@property (strong, nonatomic) id object;
14+
@property (assign, nonatomic) BOOL mustRevalidate;
15+
@property (assign, nonatomic) BOOL isOfflineVersion;
16+
@property (strong, nonatomic) NSMutableDictionary* revalidateHeaders;
17+
1318
@end

‎JSONModel/JSONModelNetworking/JSONCacheResponse.m‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,16 @@
1010

1111
@implementation JSONCacheResponse
1212

13+
-(instancetype)init
14+
{
15+
self = [super init];
16+
if (self!=nil) {
17+
self.object = nil;
18+
self.isOfflineVersion = YES;
19+
self.mustRevalidate = NO;
20+
self.revalidateHeaders = [NSMutableDictionary dictionaryWithCapacity:2];
21+
}
22+
return self;
23+
}
24+
1325
@end

‎JSONModel/JSONModelNetworking/JSONHTTPClient.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ extern NSString* const kContentTypeAutomatic;
3232
extern NSString* const kContentTypeJSON;
3333
extern NSString* const kContentTypeWWWEncoded;
3434

35+
extern NSData* kUseCachedObjectResponse;
36+
3537
/**
3638
* A block type to handle incoming JSONModel instance and an error.
3739
* You pass it to methods which create a model asynchroniously. When the operation is finished

0 commit comments

Comments
 (0)