samhuri.net-ios/Pods/PromiseKit/objc/PMKPromise.m
2014-10-18 14:19:51 -07:00

567 lines
18 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#import <assert.h>
#import <dispatch/dispatch.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSError.h>
#import <Foundation/NSException.h>
#import <Foundation/NSKeyValueCoding.h>
#import <Foundation/NSMethodSignature.h>
#import <Foundation/NSOperation.h>
#import <Foundation/NSPointerArray.h>
#import "Private/NSMethodSignatureForBlock.m"
#import "PromiseKit/Promise.h"
#import <string.h>
#define IsPromise(o) ([o isKindOfClass:[PMKPromise class]])
#define IsError(o) ([o isKindOfClass:[NSError class]])
#define PMKE(txt) [NSException exceptionWithName:@"PromiseKit" reason:@"PromiseKit: " txt userInfo:nil]
static const id PMKNull = @"PMKNull";
@interface PMKError : NSError
{ @public BOOL consumed; }
+ (instancetype):(id)foo;
@end
// deprecated
NSString const*const PMKThrown = PMKUnderlyingExceptionKey;
/**
`then` and `catch` are method-signature tolerant, this function calls
the block correctly and normalizes the return value to `id`.
*/
static id safely_call_block(id frock, id result) {
assert(frock);
if (result == PMKNull)
result = nil;
@try {
NSMethodSignature *sig = NSMethodSignatureForBlock(frock);
const NSUInteger nargs = sig.numberOfArguments;
const char rtype = sig.methodReturnType[0];
#define call_block_with_rtype(type) ({^type{ \
switch (nargs) { \
case 1: \
return ((type(^)(void))frock)(); \
case 2: { \
const id arg = [result class] == [PMKArray class] ? result[0] : result; \
return ((type(^)(id))frock)(arg); \
} \
case 3: { \
type (^block)(id, id) = frock; \
return [result class] == [PMKArray class] \
? block(result[0], result[1]) \
: block(result, nil); \
} \
case 4: { \
type (^block)(id, id, id) = frock; \
return [result class] == [PMKArray class] \
? block(result[0], result[1], result[2]) \
: block(result, nil, nil); \
} \
default: \
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"PromiseKit: The provided blocks argument count is unsupported." userInfo:nil]; \
}}();})
switch (rtype) {
case 'v':
call_block_with_rtype(void);
return PMKNull;
case '@':
return call_block_with_rtype(id) ?: PMKNull;
case '*': {
char *str = call_block_with_rtype(char *);
return str ? @(str) : PMKNull;
}
case 'c': return @(call_block_with_rtype(char));
case 'i': return @(call_block_with_rtype(int));
case 's': return @(call_block_with_rtype(short));
case 'l': return @(call_block_with_rtype(long));
case 'q': return @(call_block_with_rtype(long long));
case 'C': return @(call_block_with_rtype(unsigned char));
case 'I': return @(call_block_with_rtype(unsigned int));
case 'S': return @(call_block_with_rtype(unsigned short));
case 'L': return @(call_block_with_rtype(unsigned long));
case 'Q': return @(call_block_with_rtype(unsigned long long));
case 'f': return @(call_block_with_rtype(float));
case 'd': return @(call_block_with_rtype(double));
case 'B': return @(call_block_with_rtype(_Bool));
case '^':
if (strcmp(sig.methodReturnType, "^v") == 0) {
call_block_with_rtype(void);
return PMKNull;
}
// else fall through!
default:
@throw PMKE(@"Unsupported method signature… Why not fork and fix?");
}
} @catch (id e) {
#ifdef PMK_RETHROW_LIKE_A_MOFO
if ([e isKindOfClass:[NSException class]] && (
[e name] == NSGenericException ||
[e name] == NSRangeException ||
[e name] == NSInvalidArgumentException ||
[e name] == NSInternalInconsistencyException ||
[e name] == NSObjectInaccessibleException ||
[e name] == NSObjectNotAvailableException ||
[e name] == NSDestinationInvalidException ||
[e name] == NSPortTimeoutException ||
[e name] == NSInvalidSendPortException ||
[e name] == NSInvalidReceivePortException ||
[e name] == NSPortSendException ||
[e name] == NSPortReceiveException))
@throw e;
#endif
return [PMKError:e];
}
}
@implementation PMKPromise {
/**
We have public @implementation instance variables so PMKResolve()
can fulfill promises. Our usage is like the C++ `friend` keyword.
*/
@public
dispatch_queue_t _promiseQueue;
NSMutableArray *_handlers;
id _result;
}
- (instancetype)init {
@throw PMKE(@"init is not a valid initializer for PMKPromise");
return nil;
}
#if OS_OBJECT_USE_OBJC == 0
- (void)dealloc {
dispatch_release(_promiseQueue);
}
#endif
- (PMKPromise *(^)(id))then {
return ^(id block){
return self.thenOn(dispatch_get_main_queue(), block);
};
}
- (PMKPromise *(^)(id))catch {
return ^(id block){
return self.catchOn(dispatch_get_main_queue(), block);
};
}
- (PMKPromise *(^)(dispatch_block_t))finally {
return ^(dispatch_block_t block) {
return self.finallyOn(dispatch_get_main_queue(), block);
};
}
typedef PMKPromise *(^PMKResolveOnQueueBlock)(dispatch_queue_t, id block);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
// Convoluted helper method that returns a block that is called
// from a thenOn/catchOn/finallyOn. It returns a block that when
// executed calls the users block with our result. The method
// takes two blocks that allow the callee to alter the behavior
// when calling the users block. The first for the already-
// resolved state, the second for the pending state.
- (id)resolved:(PMKResolveOnQueueBlock(^)(id result))mkresolvedCallback
pending:(void(^)(id result, PMKPromise *next, dispatch_queue_t q, id block, PMKPromiseFulfiller resolver))mkpendingCallback
{
__block PMKResolveOnQueueBlock callBlock;
__block id result;
dispatch_sync(_promiseQueue, ^{
if ((result = _result))
return;
callBlock = ^(dispatch_queue_t q, id block) {
// HACK we seem to expose some bug in ARC where this block can
// be an NSStackBlock which then gets deallocated by the time
// we get around to using it. So we force it to be malloc'd.
block = [block copy];
__block PMKPromise *next = nil;
dispatch_barrier_sync(_promiseQueue, ^{
if ((result = _result))
return;
__block PMKPromiseFulfiller resolver;
next = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
resolver = ^(id o){
if (IsError(o)) reject(o); else fulfill(o);
};
}];
[_handlers addObject:^(id value){
mkpendingCallback(value, next, q, block, resolver);
}];
});
// next can still be `nil` if the promise was resolved after
// 1) `-thenOn` read it and decided which block to return; and
// 2) the call to the block.
return next ?: mkresolvedCallback(result)(q, block);
};
});
// We could just always return the above block, but then every caller would
// trigger a barrier_sync on the promise queue. Instead, if we know that the
// promise is resolved (since that makes it immutable), we can return a simpler
// block that doesn't use a barrier in those cases.
return callBlock ?: mkresolvedCallback(result);
}
#pragma clang diagnostic pop
- (PMKResolveOnQueueBlock)thenOn {
return [self resolved:^(id result) {
if (IsPromise(result))
return ((PMKPromise *)result).thenOn;
if (IsError(result)) return ^(dispatch_queue_t q, id block) {
return [PMKPromise promiseWithValue:result];
};
return ^(dispatch_queue_t q, id block) {
// HACK we seem to expose some bug in ARC where this block can
// be an NSStackBlock which then gets deallocated by the time
// we get around to using it. So we force it to be malloc'd.
block = [block copy];
return dispatch_promise_on(q, ^{
return safely_call_block(block, result);
});
};
}
pending:^(id result, PMKPromise *next, dispatch_queue_t q, id block, PMKPromiseFulfiller resolve) {
if (IsError(result))
return PMKResolve(next, result);
dispatch_async(q, ^{
resolve(safely_call_block(block, result));
});
}];
}
- (PMKResolveOnQueueBlock)catchOn {
return [self resolved:^(id result) {
if (IsPromise(result))
return ((PMKPromise *)result).catchOn;
if (IsError(result)) return ^(dispatch_queue_t q, id block) {
// HACK we seem to expose some bug in ARC where this block can
// be an NSStackBlock which then gets deallocated by the time
// we get around to using it. So we force it to be malloc'd.
block = [block copy];
return dispatch_promise_on(q, ^{
id rv = safely_call_block(block, result);
if (rv != result)
// if the handler rethrows the same error, it is not consumed
((PMKError *)result)->consumed = YES;
return rv;
});
};
return ^(dispatch_queue_t q, id block) {
return [PMKPromise promiseWithValue:result];
};
}
pending:^(id result, PMKPromise *next, dispatch_queue_t q, id block, PMKPromiseFulfiller resolve) {
if (IsError(result)) {
dispatch_async(q, ^{
id rv = safely_call_block(block, result);
if (rv != result)
// if the handler rethrows the same error, it is not consumed
((PMKError *)result)->consumed = YES;
resolve(rv);
});
} else
PMKResolve(next, result);
}];
}
- (PMKPromise *(^)(dispatch_queue_t, dispatch_block_t))finallyOn {
return [self resolved:^(id passthru) {
if (IsPromise(passthru))
return ((PMKPromise *)passthru).finallyOn;
return ^(dispatch_queue_t q, dispatch_block_t block) {
// HACK we seem to expose some bug in ARC where this block can
// be an NSStackBlock which then gets deallocated by the time
// we get around to using it. So we force it to be malloc'd.
block = [block copy];
return dispatch_promise_on(q, ^{
block();
return passthru;
});
};
} pending:^(id passthru, PMKPromise *next, dispatch_queue_t q, dispatch_block_t block, PMKPromiseFulfiller resolve) {
dispatch_async(q, ^{
@try {
block();
resolve(passthru);
} @catch (id e) {
resolve(e);
}
});
}];
}
+ (PMKPromise *)promiseWithValue:(id)value {
PMKPromise *p = [PMKPromise alloc];
p->_promiseQueue = PMKCreatePromiseQueue();
p->_result = value ?: PMKNull;
return p;
}
static dispatch_queue_t PMKCreatePromiseQueue() {
return dispatch_queue_create("org.promiseKit.Q", DISPATCH_QUEUE_CONCURRENT);
}
static id PMKGetResult(PMKPromise *this) {
__block id result;
dispatch_sync(this->_promiseQueue, ^{
result = this->_result;
});
return result;
}
static NSArray *PMKSetResult(PMKPromise *this, id result) {
__block NSArray *handlers;
dispatch_barrier_sync(this->_promiseQueue, ^{
handlers = this->_handlers;
this->_result = result ?: PMKNull;
this->_handlers = nil;
});
return handlers;
}
static void PMKResolve(PMKPromise *this, id result) {
void (^set)(id) = ^(id r){
NSArray *handlers = PMKSetResult(this, r);
for (void (^handler)(id) in handlers)
handler(r);
};
if (IsPromise(result)) {
PMKPromise *next = result;
dispatch_barrier_sync(next->_promiseQueue, ^{
id nextResult = next->_result;
if (nextResult == nil) { // ie. pending
[next->_handlers addObject:^(id o){
PMKResolve(this, o);
}];
} else
set(nextResult);
});
} else
set(result);
}
+ (instancetype)new:(void(^)(PMKPromiseFulfiller, PMKPromiseRejecter))block {
PMKPromise *this = [self alloc];
this->_promiseQueue = PMKCreatePromiseQueue();
this->_handlers = [NSMutableArray new];
id fulfiller = ^(id value){
if (PMKGetResult(this))
return NSLog(@"PromiseKit: Promise already resolved");
if (IsError(value))
@throw PMKE(@"You may not fulfill a Promise with an NSError");
PMKResolve(this, value);
};
id rejecter = ^(id error){
if (PMKGetResult(this))
return NSLog(@"PromiseKit: Promise already resolved");
if (IsPromise(error)) {
if ([error rejected]) {
error = ((PMKPromise *)error).value;
} else
@throw PMKE(@"You may not reject a Promise with a Promise");
}
if (!error)
error = [PMKError errorWithDomain:PMKErrorDomain code:PMKUnknownError userInfo:nil];
if (!IsError(error)) {
NSLog(@"PromiseKit: Warning: Reject promises with NSErrors!");
error = [PMKError errorWithDomain:PMKErrorDomain code:PMKInvalidUsageError userInfo:@{
NSLocalizedDescriptionKey: [error description]
}];
}
PMKResolve(this, [PMKError:error]);
};
@try {
block(fulfiller, rejecter);
} @catch (id e) {
PMKSetResult(this, [PMKError:e]);
}
return this;
}
- (BOOL)pending {
id result = PMKGetResult(self);
if (IsPromise(result)) {
return [result pending];
} else
return result == nil;
}
- (BOOL)resolved {
return PMKGetResult(self) != nil;
}
- (BOOL)fulfilled {
id result = PMKGetResult(self);
return result != nil && !IsError(result);
}
- (BOOL)rejected {
id result = PMKGetResult(self);
return result != nil && IsError(result);
}
- (id)value {
id result = PMKGetResult(self);
if (IsPromise(result))
return [(PMKPromise *)result value];
if ([result isKindOfClass:[PMKArray class]])
return result[0];
if (result == PMKNull)
return nil;
else
return result;
}
- (NSString *)description {
__block id result;
__block NSUInteger handlerCount;
dispatch_sync(_promiseQueue, ^{
result = self->_result;
handlerCount = self->_handlers.count;
});
BOOL pending = IsPromise(result) ? [result pending] : (result == nil);
BOOL resolved = result != nil;
BOOL fulfilled = resolved && !IsError(result);
BOOL rejected = resolved && IsError(result);
if (pending)
return [NSString stringWithFormat:@"Promise: %lu pending handlers", (unsigned long)handlerCount];
if (rejected)
return [NSString stringWithFormat:@"Promise: rejected: %@", result];
assert(fulfilled);
return [NSString stringWithFormat:@"Promise: fulfilled: %@", result];
}
@end
PMKPromise *dispatch_promise(id block) {
return dispatch_promise_on(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
}
PMKPromise *dispatch_promise_on(dispatch_queue_t queue, id block) {
return [PMKPromise new:^(void(^fulfiller)(id), void(^rejecter)(id)){
dispatch_async(queue, ^{
id result = safely_call_block(block, nil);
if (IsError(result))
rejecter(result);
else
fulfiller(result);
});
}];
}
@implementation PMKArray { NSUInteger count; id objs[3]; }
+ (instancetype)arrayWithCount:(NSUInteger)count, ... {
PMKArray *this = [self new];
this->count = count;
va_list args;
va_start(args, count);
for (NSUInteger x = 0; x < count; ++x)
this->objs[x] = va_arg(args, id);
va_end(args);
return this;
}
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
if (count <= idx) {
// this check is necessary due to lack of checks in `safely_call_block`
return nil;
}
return objs[idx];
}
@end
@implementation PMKError
+ (instancetype):(id)foo {
if ([foo isKindOfClass:[PMKError class]])
return foo;
if ([foo isKindOfClass:[NSError class]])
return [PMKError errorWithDomain:[foo domain] ?: PMKErrorDomain code:[foo code] userInfo:[foo userInfo]];
else {
id userInfo = [NSMutableDictionary new];
userInfo[PMKUnderlyingExceptionKey] = foo;
userInfo[NSLocalizedDescriptionKey] = [foo isKindOfClass:[NSException class]]
? [foo reason]
: [foo description];
return [PMKError errorWithDomain:PMKErrorDomain code:PMKUnhandledExceptionError userInfo:userInfo];
}
}
- (void)dealloc {
if (!consumed)
NSLog(@"PromiseKit: Unhandled error: %@", self);
}
@end
NSOperationQueue *PMKOperationQueue() {
static NSOperationQueue *q;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
q = [NSOperationQueue new];
q.name = @"org.promisekit.Q";
});
return q;
}
void *PMKManualReferenceAssociatedObject = &PMKManualReferenceAssociatedObject;