mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-10-29 20:10:41 -04:00
501 lines
19 KiB
Objective-C
501 lines
19 KiB
Objective-C
//
|
|
// FLEXNetworkRecorder.m
|
|
// Flipboard
|
|
//
|
|
// Created by Ryan Olson on 2/4/15.
|
|
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
|
//
|
|
|
|
#import "FLEXNetworkRecorder.h"
|
|
#import "FLEXNetworkCurlLogger.h"
|
|
#import "FLEXNetworkTransaction.h"
|
|
#import "FLEXUtility.h"
|
|
#import "FLEXResources.h"
|
|
#import "NSUserDefaults+FLEX.h"
|
|
#import "OSCache.h"
|
|
|
|
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
|
|
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
|
|
NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
|
|
NSString *const kFLEXNetworkRecorderTransactionsClearedNotification = @"kFLEXNetworkRecorderTransactionsClearedNotification";
|
|
|
|
NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.responseCacheLimit";
|
|
|
|
@interface FLEXNetworkRecorder ()
|
|
|
|
@property (nonatomic) OSCache *restCache;
|
|
@property (atomic) NSMutableArray<FLEXHTTPTransaction *> *orderedHTTPTransactions;
|
|
@property (atomic) NSMutableArray<FLEXWebsocketTransaction *> *orderedWSTransactions;
|
|
@property (atomic) NSMutableArray<FLEXFirebaseTransaction *> *orderedFirebaseTransactions;
|
|
@property (atomic) NSMutableDictionary<NSString *, __kindof FLEXNetworkTransaction *> *requestIDsToTransactions;
|
|
@property (nonatomic) dispatch_queue_t queue;
|
|
|
|
@end
|
|
|
|
@implementation FLEXNetworkRecorder
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
self.restCache = [OSCache new];
|
|
NSUInteger responseCacheLimit = [[NSUserDefaults.standardUserDefaults
|
|
objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue
|
|
];
|
|
|
|
// Default to 25 MB max. The cache will purge earlier if there is memory pressure.
|
|
self.restCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
|
|
[self.restCache setTotalCostLimit:responseCacheLimit];
|
|
|
|
self.orderedWSTransactions = [NSMutableArray new];
|
|
self.orderedHTTPTransactions = [NSMutableArray new];
|
|
self.orderedFirebaseTransactions = [NSMutableArray new];
|
|
self.requestIDsToTransactions = [NSMutableDictionary new];
|
|
self.hostDenylist = NSUserDefaults.standardUserDefaults.flex_networkHostDenylist.mutableCopy;
|
|
|
|
// Serial queue used because we use mutable objects that are not thread safe
|
|
self.queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
+ (instancetype)defaultRecorder {
|
|
static FLEXNetworkRecorder *defaultRecorder = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
defaultRecorder = [self new];
|
|
});
|
|
|
|
return defaultRecorder;
|
|
}
|
|
|
|
#pragma mark - Public Data Access
|
|
|
|
- (NSUInteger)responseCacheByteLimit {
|
|
return self.restCache.totalCostLimit;
|
|
}
|
|
|
|
- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit {
|
|
self.restCache.totalCostLimit = responseCacheByteLimit;
|
|
[NSUserDefaults.standardUserDefaults
|
|
setObject:@(responseCacheByteLimit)
|
|
forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey
|
|
];
|
|
}
|
|
|
|
- (NSArray<FLEXHTTPTransaction *> *)HTTPTransactions {
|
|
return self.orderedHTTPTransactions.copy;
|
|
}
|
|
|
|
- (NSArray<FLEXWebsocketTransaction *> *)websocketTransactions {
|
|
return self.orderedWSTransactions.copy;
|
|
}
|
|
|
|
- (NSArray<FLEXFirebaseTransaction *> *)firebaseTransactions {
|
|
return self.orderedFirebaseTransactions.copy;
|
|
}
|
|
|
|
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction {
|
|
return [self.restCache objectForKey:transaction.requestID];
|
|
}
|
|
|
|
- (void)clearRecordedActivity {
|
|
dispatch_async(self.queue, ^{
|
|
[self.restCache removeAllObjects];
|
|
[self.orderedWSTransactions removeAllObjects];
|
|
[self.orderedHTTPTransactions removeAllObjects];
|
|
[self.orderedFirebaseTransactions removeAllObjects];
|
|
[self.requestIDsToTransactions removeAllObjects];
|
|
|
|
[self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
|
|
});
|
|
}
|
|
|
|
- (void)clearRecordedActivity:(FLEXNetworkTransactionKind)kind matching:(NSString *)query {
|
|
dispatch_async(self.queue, ^{
|
|
switch (kind) {
|
|
case FLEXNetworkTransactionKindFirebase: {
|
|
[self.orderedFirebaseTransactions flex_filter:^BOOL(FLEXFirebaseTransaction *obj, NSUInteger idx) {
|
|
return ![obj matchesQuery:query];
|
|
}];
|
|
break;
|
|
}
|
|
case FLEXNetworkTransactionKindREST: {
|
|
NSArray<FLEXHTTPTransaction *> *toRemove;
|
|
toRemove = [self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *obj, NSUInteger idx) {
|
|
return [obj matchesQuery:query];
|
|
}];
|
|
|
|
// Remove from cache
|
|
for (FLEXHTTPTransaction *t in toRemove) {
|
|
[self.restCache removeObjectForKey:t.requestID];
|
|
}
|
|
|
|
// Remove from list
|
|
[self.orderedHTTPTransactions removeObjectsInArray:toRemove];
|
|
|
|
break;
|
|
}
|
|
case FLEXNetworkTransactionKindWebsockets: {
|
|
[self.orderedWSTransactions flex_filter:^BOOL(FLEXWebsocketTransaction *obj, NSUInteger idx) {
|
|
return ![obj matchesQuery:query];
|
|
}];
|
|
break;
|
|
}
|
|
}
|
|
|
|
[self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
|
|
});
|
|
}
|
|
|
|
- (void)clearExcludedTransactions {
|
|
dispatch_sync(self.queue, ^{
|
|
self.orderedHTTPTransactions = ({
|
|
[self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *ta, NSUInteger idx) {
|
|
NSString *host = ta.request.URL.host;
|
|
for (NSString *excluded in self.hostDenylist) {
|
|
if ([host hasSuffix:excluded]) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}];
|
|
});
|
|
});
|
|
}
|
|
|
|
- (void)synchronizeDenylist {
|
|
NSUserDefaults.standardUserDefaults.flex_networkHostDenylist = self.hostDenylist;
|
|
}
|
|
|
|
#pragma mark - Network Events
|
|
|
|
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
|
|
request:(NSURLRequest *)request
|
|
redirectResponse:(NSURLResponse *)redirectResponse {
|
|
for (NSString *host in self.hostDenylist) {
|
|
if ([request.URL.host hasSuffix:host]) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
FLEXHTTPTransaction *transaction = [FLEXHTTPTransaction request:request identifier:requestID];
|
|
|
|
// Before async block to keep times accurate
|
|
if (redirectResponse) {
|
|
[self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
|
|
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
|
|
}
|
|
|
|
// A redirect is always a new request
|
|
dispatch_async(self.queue, ^{
|
|
[self.orderedHTTPTransactions insertObject:transaction atIndex:0];
|
|
self.requestIDsToTransactions[requestID] = transaction;
|
|
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response {
|
|
// Before async block to stay accurate
|
|
NSDate *responseDate = [NSDate date];
|
|
|
|
dispatch_async(self.queue, ^{
|
|
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.response = response;
|
|
transaction.state = FLEXNetworkTransactionStateReceivingData;
|
|
transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
|
|
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.receivedDataLength += dataLength;
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody {
|
|
NSDate *finishedDate = [NSDate date];
|
|
|
|
dispatch_async(self.queue, ^{
|
|
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.state = FLEXNetworkTransactionStateFinished;
|
|
transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
|
|
|
|
BOOL shouldCache = responseBody.length > 0;
|
|
if (!self.shouldCacheMediaResponses) {
|
|
NSArray<NSString *> *ignoredMIMETypePrefixes = @[ @"audio", @"image", @"video" ];
|
|
for (NSString *ignoredPrefix in ignoredMIMETypePrefixes) {
|
|
shouldCache = shouldCache && ![transaction.response.MIMEType hasPrefix:ignoredPrefix];
|
|
}
|
|
}
|
|
|
|
if (shouldCache) {
|
|
[self.restCache setObject:responseBody forKey:requestID cost:responseBody.length];
|
|
}
|
|
|
|
NSString *mimeType = transaction.response.MIMEType;
|
|
if ([mimeType hasPrefix:@"image/"] && responseBody.length > 0) {
|
|
// Thumbnail image previews on a separate background queue
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
NSInteger maxPixelDimension = UIScreen.mainScreen.scale * 32.0;
|
|
transaction.thumbnail = [FLEXUtility
|
|
thumbnailedImageWithMaxPixelDimension:maxPixelDimension
|
|
fromImageData:responseBody
|
|
];
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
} else if ([mimeType isEqual:@"application/json"]) {
|
|
transaction.thumbnail = FLEXResources.jsonIcon;
|
|
} else if ([mimeType isEqual:@"text/plain"]){
|
|
transaction.thumbnail = FLEXResources.textPlainIcon;
|
|
} else if ([mimeType isEqual:@"text/html"]) {
|
|
transaction.thumbnail = FLEXResources.htmlIcon;
|
|
} else if ([mimeType isEqual:@"application/x-plist"]) {
|
|
transaction.thumbnail = FLEXResources.plistIcon;
|
|
} else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
|
|
transaction.thumbnail = FLEXResources.binaryIcon;
|
|
} else if ([mimeType containsString:@"javascript"]) {
|
|
transaction.thumbnail = FLEXResources.jsIcon;
|
|
} else if ([mimeType containsString:@"xml"]) {
|
|
transaction.thumbnail = FLEXResources.xmlIcon;
|
|
} else if ([mimeType hasPrefix:@"audio"]) {
|
|
transaction.thumbnail = FLEXResources.audioIcon;
|
|
} else if ([mimeType hasPrefix:@"video"]) {
|
|
transaction.thumbnail = FLEXResources.videoIcon;
|
|
} else if ([mimeType hasPrefix:@"text"]) {
|
|
transaction.thumbnail = FLEXResources.textIcon;
|
|
}
|
|
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.state = FLEXNetworkTransactionStateFailed;
|
|
transaction.duration = -[transaction.startTime timeIntervalSinceNow];
|
|
transaction.error = error;
|
|
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.requestMechanism = mechanism;
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
#pragma mark - Websocket Events
|
|
|
|
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXWebsocketTransaction *send = [FLEXWebsocketTransaction
|
|
withMessage:message task:task direction:FLEXWebsocketOutgoing
|
|
];
|
|
|
|
[self.orderedWSTransactions insertObject:send atIndex:0];
|
|
[self postNewTransactionNotificationWithTransaction:send];
|
|
});
|
|
}
|
|
|
|
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXWebsocketTransaction *send = [self.orderedWSTransactions flex_firstWhere:^BOOL(FLEXWebsocketTransaction *t) {
|
|
return t.message == message;
|
|
}];
|
|
send.error = error;
|
|
send.state = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished;
|
|
|
|
[self postUpdateNotificationForTransaction:send];
|
|
});
|
|
}
|
|
|
|
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXWebsocketTransaction *receive = [FLEXWebsocketTransaction
|
|
withMessage:message task:task direction:FLEXWebsocketIncoming
|
|
];
|
|
|
|
[self.orderedWSTransactions insertObject:receive atIndex:0];
|
|
[self postNewTransactionNotificationWithTransaction:receive];
|
|
});
|
|
}
|
|
|
|
#pragma mark - Firebase, Reading
|
|
|
|
- (void)recordFIRQueryWillFetch:(FIRQuery *)query withTransactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction queryFetch:query];
|
|
self.requestIDsToTransactions[transactionID] = transaction;
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRDocumentWillFetch:(FIRDocumentReference *)document withTransactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction documentFetch:document];
|
|
self.requestIDsToTransactions[transactionID] = transaction;
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRQueryDidFetch:(FIRQuerySnapshot *)response error:(NSError *)error transactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = self.requestIDsToTransactions[transactionID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.error = error;
|
|
transaction.documents = response.documents;
|
|
transaction.state = FLEXNetworkTransactionStateFinished;
|
|
[self.orderedFirebaseTransactions insertObject:transaction atIndex:0];
|
|
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRDocumentDidFetch:(FIRDocumentSnapshot *)response error:(NSError *)error transactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = self.requestIDsToTransactions[transactionID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.error = error;
|
|
transaction.documents = response ? @[response] : @[];
|
|
transaction.state = FLEXNetworkTransactionStateFinished;
|
|
[self.orderedFirebaseTransactions insertObject:transaction atIndex:0];
|
|
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
#pragma mark Firebase, Writing
|
|
|
|
- (void)recordFIRWillSetData:(FIRDocumentReference *)doc
|
|
data:(NSDictionary *)documentData
|
|
merge:(NSNumber *)yesorno
|
|
mergeFields:(NSArray *)fields
|
|
transactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction
|
|
setData:doc data:documentData merge:yesorno mergeFields:fields
|
|
];
|
|
self.requestIDsToTransactions[transactionID] = transaction;
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRWillUpdateData:(FIRDocumentReference *)doc fields:(NSDictionary *)fields
|
|
transactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction updateData:doc data:fields];
|
|
self.requestIDsToTransactions[transactionID] = transaction;
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRWillDeleteDocument:(FIRDocumentReference *)doc transactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction deleteDocument:doc];
|
|
self.requestIDsToTransactions[transactionID] = transaction;
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRWillAddDocument:(FIRCollectionReference *)initiator document:(FIRDocumentReference *)doc
|
|
transactionID:(NSString *)transactionID {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction
|
|
addDocument:initiator document:doc
|
|
];
|
|
self.requestIDsToTransactions[transactionID] = transaction;
|
|
[self postNewTransactionNotificationWithTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (void)recordFIRDidSetData:(NSError *)error transactionID:(NSString *)transactionID {
|
|
[self firebaseTransaction:transactionID didUpdate:error];
|
|
}
|
|
|
|
- (void)recordFIRDidUpdateData:(NSError *)error transactionID:(NSString *)transactionID {
|
|
[self firebaseTransaction:transactionID didUpdate:error];
|
|
}
|
|
|
|
- (void)recordFIRDidDeleteDocument:(NSError *)error transactionID:(NSString *)transactionID {
|
|
[self firebaseTransaction:transactionID didUpdate:error];
|
|
}
|
|
|
|
- (void)recordFIRDidAddDocument:(NSError *)error transactionID:(NSString *)transactionID {
|
|
[self firebaseTransaction:transactionID didUpdate:error];
|
|
}
|
|
|
|
- (void)firebaseTransaction:(NSString *)transactionID didUpdate:(NSError *)error {
|
|
dispatch_async(self.queue, ^{
|
|
FLEXFirebaseTransaction *transaction = self.requestIDsToTransactions[transactionID];
|
|
if (!transaction) {
|
|
return;
|
|
}
|
|
|
|
transaction.error = error;
|
|
transaction.state = FLEXNetworkTransactionStateFinished;
|
|
[self.orderedFirebaseTransactions insertObject:transaction atIndex:0];
|
|
|
|
[self postUpdateNotificationForTransaction:transaction];
|
|
});
|
|
}
|
|
|
|
#pragma mark - Notification Posting
|
|
|
|
- (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction {
|
|
[self notify:kFLEXNetworkRecorderNewTransactionNotification transaction:transaction];
|
|
}
|
|
|
|
- (void)postUpdateNotificationForTransaction:(FLEXNetworkTransaction *)transaction {
|
|
[self notify:kFLEXNetworkRecorderTransactionUpdatedNotification transaction:transaction];
|
|
}
|
|
|
|
- (void)notify:(NSString *)name transaction:(FLEXNetworkTransaction *)transaction {
|
|
NSDictionary *userInfo = nil;
|
|
if (transaction) {
|
|
userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[NSNotificationCenter.defaultCenter postNotificationName:name object:self userInfo:userInfo];
|
|
});
|
|
}
|
|
|
|
@end
|