diff --git a/CustomKeyboard/Network/WJXEventSource/WJXEventSource.h b/CustomKeyboard/Network/WJXEventSource/WJXEventSource.h new file mode 100644 index 0000000..bea1857 --- /dev/null +++ b/CustomKeyboard/Network/WJXEventSource/WJXEventSource.h @@ -0,0 +1,70 @@ +// +// WJXEventSource.h +// WJXEventSource +// +// Created by JiuxingWang on 2025/2/9. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifdef __cplusplus +#define WJX_EXTERN extern "C" __attribute__((visibility ("default"))) +#else +#define WJX_EXTERN extern __attribute__((visibility ("default"))) +#endif + +/// 消息事件 +typedef NSString *WJXEventName NS_TYPED_EXTENSIBLE_ENUM; + +/// 消息事件 +WJX_EXTERN WJXEventName const WJXEventNameMessage; + +/// readyState 变化事件 +WJX_EXTERN WJXEventName const WJXEventNameReadyState; + +/// open 事件 +WJX_EXTERN WJXEventName const WJXEventNameOpen; + +/// error 事件 +WJX_EXTERN WJXEventName const WJXEventNameError; + +typedef NS_ENUM(NSUInteger, WJXEventState) { + WJXEventStateConnecting = 0, + WJXEventStateOpen, + WJXEventStateClosed, +}; + +@interface WJXEvent : NSObject + +@property (nonatomic, strong, nullable) id eventId; + +@property (nonatomic, copy, nullable) NSString *event; +@property (nonatomic, copy, nullable) NSString *data; + +@property (nonatomic, assign) WJXEventState readyState; +@property (nonatomic, strong, nullable) NSError *error; + +- (instancetype)initWithReadyState:(WJXEventState)readyState; + +@end + +typedef void(^WJXEventSourceEventHandler)(WJXEvent *event); + +@interface WJXEventSource : NSObject + +@property (nonatomic, assign) BOOL ignoreRetryAction; + +- (instancetype)initWithRquest:(NSURLRequest *)request; + +- (void)addListener:(WJXEventSourceEventHandler)listener + forEvent:(WJXEventName)eventName + queue:(nullable NSOperationQueue *)queue; + +- (void)open; +- (void)close; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/Network/WJXEventSource/WJXEventSource.m b/CustomKeyboard/Network/WJXEventSource/WJXEventSource.m new file mode 100644 index 0000000..f588e77 --- /dev/null +++ b/CustomKeyboard/Network/WJXEventSource/WJXEventSource.m @@ -0,0 +1,287 @@ +// +// WJXEventSource.m +// WJXEventSource +// +// Created by JiuxingWang on 2025/2/9. +// + +#import "WJXEventSource.h" + +/// 消息事件 +WJXEventName const WJXEventNameMessage = @"message"; + +/// readyState 变化事件 +WJXEventName const WJXEventNameReadyState = @"readyState"; + +/// open 事件 +WJXEventName const WJXEventNameOpen = @"open"; + +/// error 事件 +WJXEventName const WJXEventNameError = @"error"; + +#pragma mark - +#pragma mark WJXEvent + +@implementation WJXEvent + +- (instancetype)initWithReadyState:(WJXEventState)readyState; +{ + if (self = [super init]) { + self.readyState = readyState; + } + return self; +} + +- (NSString *)description +{ + NSString *state = nil; + switch (_readyState) { + case WJXEventStateConnecting: { + state = @"CONNECTING"; + } break; + + case WJXEventStateOpen: { + state = @"OPEN"; + } break; + + case WJXEventStateClosed: { + state = @"CLOSED"; + } break; + } + + return [NSString stringWithFormat:@"<%@: readyState: %@, id: %@; event: %@; data: %@>", [self class], state, _eventId, _event, _data]; +} + +@end + + +#pragma mark - +#pragma mark WJXEventHandler + +@interface WJXEventHandler : NSObject + +@property (nonatomic, copy, nonnull) WJXEventSourceEventHandler handler; +@property (nonatomic, strong, nullable) NSOperationQueue *queue; + +@end + +@implementation WJXEventHandler + +- (instancetype)initWithHandler:(WJXEventSourceEventHandler)handler queue:(NSOperationQueue *)queue +{ + if (self = [super init]) { + self.handler = handler; + self.queue = queue; + } + return self; +} + +@end + + +#pragma mark - +#pragma mark WJXEventSource + +@interface WJXEventSource () + +@property (nonatomic, strong) NSMutableURLRequest *request; +@property (nonatomic, strong) NSMutableDictionary *> *listeners; + +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSURLSessionDataTask *dataTask; +@property (nonatomic, copy) NSString *lastEventId; +@property (nonatomic, assign) NSTimeInterval retryInterval; + +@property (nonatomic, assign) BOOL closedByUser; +@property (nonatomic, strong) NSMutableData *buffer; + +@end + +@implementation WJXEventSource + +- (instancetype)initWithRquest:(NSURLRequest *)request; +{ + if (self = [super init]) { + self.request = [request mutableCopy]; + self.listeners = [NSMutableDictionary dictionary]; + self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:self delegateQueue:NSOperationQueue.mainQueue]; + self.buffer = [NSMutableData data]; + } + return self; +} + +- (void)dealloc +{ + [_session finishTasksAndInvalidate]; +} + +- (void)addListener:(WJXEventSourceEventHandler)listener + forEvent:(WJXEventName)eventName + queue:(nullable NSOperationQueue *)queue; +{ + if (nil == listener) { + return; + } + + NSMutableArray *listeners = self.listeners[eventName]; + if (nil == listeners) { + self.listeners[eventName] = listeners = [NSMutableArray array]; + } + [listeners addObject:[[WJXEventHandler alloc] initWithHandler:listener queue:queue]]; +} + +- (void)open; +{ + if (_lastEventId.length) { + [_request setValue:_lastEventId forHTTPHeaderField:@"Last-Event-ID"]; + } + + self.dataTask = [_session dataTaskWithRequest:_request]; + [_dataTask resume]; + + WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateConnecting]; + [self _dispatchEvent:event forName:WJXEventNameReadyState]; +} + +- (void)close; +{ + self.closedByUser = YES; + [_dataTask cancel]; + [_session finishTasksAndInvalidate]; + _buffer = [NSMutableData data]; +} + + +#pragma mark - +#pragma mark NSURLSessionDataDelegate + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler; +{ + NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; + if (200 == HTTPResponse.statusCode) { + WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateOpen]; + [self _dispatchEvent:event forName:WJXEventNameReadyState]; + [self _dispatchEvent:event forName:WJXEventNameOpen]; + } + + if (nil != completionHandler) { + completionHandler(NSURLSessionResponseAllow); + } +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data; +{ + [_buffer appendData:data]; + [self _processBuffer]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didCompleteWithError:(nullable NSError *)error; +{ + if (_closedByUser) { + return; + } + + WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateClosed]; + if (nil == (event.error = error)) { + event.error = [NSError errorWithDomain:@"WJXEventSource" code:event.readyState userInfo:@{ + NSLocalizedDescriptionKey: @"Connection with the event source was closed without error", + }]; + } + [self _dispatchEvent:event forName:WJXEventNameReadyState]; + + if (nil != error) { + [self _dispatchEvent:event forName:WJXEventNameError]; + if (!_ignoreRetryAction) { + [self performSelector:@selector(open) withObject:nil afterDelay:_retryInterval]; + } + } +} + + +#pragma mark - +#pragma mark Private + +- (void)_processBuffer +{ + NSData *separatorLFLFData = [NSData dataWithBytes:"\n\n" length:2]; + + NSRange range = [_buffer rangeOfData:separatorLFLFData options:kNilOptions range:(NSRange) { + .length = _buffer.length + }]; + + while (NSNotFound != range.location) { + // Extract event data + NSData *eventData = [_buffer subdataWithRange:(NSRange) { + .length = range.location + }]; + [_buffer replaceBytesInRange:(NSRange) { + .length = range.location + 2 + } withBytes:NULL length:0]; + + [self _parseEventData:eventData]; + + // Look for next event + range = [_buffer rangeOfData:separatorLFLFData options:kNilOptions range:(NSRange) { + .length = _buffer.length + }]; + } +} + +- (void)_parseEventData:(NSData *)data +{ + WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateOpen]; + + NSString *eventString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray *lines = [eventString componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet]; + for (NSString *line in lines) { + if ([line hasPrefix:@"id:"]) { + event.eventId = [[line substringFromIndex:3] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + } else if ([line hasPrefix:@"event:"]) { + event.event = [[line substringFromIndex:6] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + } else if ([line hasPrefix:@"data:"]) { + NSString *data = [[line substringFromIndex:5] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + event.data = event.data ? [event.data stringByAppendingFormat:@"\n%@", data] : data; + } else if ([line hasPrefix:@"retry:"]) { + NSString *retryString = [[line substringFromIndex:6] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + self.retryInterval = [retryString doubleValue] / 1000; + } + } + + if (event.eventId) { + self.lastEventId = event.eventId; + } + + [self _dispatchEvent:event forName:WJXEventNameMessage]; +} + +- (void)_dispatchEvent:(WJXEvent *)event forName:(WJXEventName)name +{ + NSMutableArray *listeners = self.listeners[name]; + [listeners enumerateObjectsUsingBlock:^(WJXEventHandler * _Nonnull handler, NSUInteger idx, BOOL * _Nonnull stop) { + NSOperationQueue *queue = handler.queue ?: NSOperationQueue.mainQueue; + [queue addOperationWithBlock:^{ + handler.handler(event); + }]; + }]; +} + + +#pragma mark - +#pragma mark Setters + +- (void)setDataTask:(NSURLSessionDataTask *)dataTask +{ + self.closedByUser = YES; { + [_dataTask cancel]; + _dataTask = dataTask; + } self.closedByUser = NO; +} + +@end diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index e06a4fd..310cd4f 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 0498BD8C2EE69E15006CC1D5 /* KBTagItemModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */; }; 0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; }; 0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; }; + 0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */; }; 049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */; }; 049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB20D2EC1CD2800FAB05D /* KBAlert.m */; }; 049FB2112EC1F72F00FAB05D /* KBMyListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2102EC1F72F00FAB05D /* KBMyListCell.m */; }; @@ -405,6 +406,8 @@ 0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTagItemModel.m; sourceTree = ""; }; 0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyMainModel.h; sourceTree = ""; }; 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyMainModel.m; sourceTree = ""; }; + 0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WJXEventSource.h; sourceTree = ""; }; + 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WJXEventSource.m; sourceTree = ""; }; 049FB2092EC1C13800FAB05D /* KBSkinBottomActionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinBottomActionView.h; sourceTree = ""; }; 049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinBottomActionView.m; sourceTree = ""; }; 049FB20C2EC1CD2800FAB05D /* KBAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAlert.h; sourceTree = ""; }; @@ -909,6 +912,15 @@ path = VM; sourceTree = ""; }; + 0498BDD92EE7ECEA006CC1D5 /* WJXEventSource */ = { + isa = PBXGroup; + children = ( + 0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */, + 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */, + ); + path = WJXEventSource; + sourceTree = ""; + }; 049FB2162EC20A6600FAB05D /* BMLongPressDragCellCollectionView */ = { isa = PBXGroup; children = ( @@ -1490,6 +1502,7 @@ A1B2C3E52EB0C0A100000001 /* Network */ = { isa = PBXGroup; children = ( + 0498BDD92EE7ECEA006CC1D5 /* WJXEventSource */, A1B2C3E02EB0C0A100000001 /* KBNetworkManager.h */, A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */, 049FB2302EC45A0000FAB05D /* KBStreamFetcher.h */, @@ -1716,6 +1729,7 @@ 04FC95672EB0546C007BD342 /* KBKey.m in Sources */, A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */, 0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */, + 0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */, 04D1F6B22EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */, A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */, 04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */,