This commit is contained in:
2025-12-09 13:59:32 +08:00
parent 0400d2020b
commit 1b2b0c1143
3 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
//
// WJXEventSource.h
// WJXEventSource
//
// Created by JiuxingWang on 2025/2/9.
//
#import <Foundation/Foundation.h>
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

View File

@@ -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 () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableURLRequest *request;
@property (nonatomic, strong) NSMutableDictionary<WJXEventName, NSMutableArray<WJXEventHandler *> *> *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<WJXEventHandler *> *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

View File

@@ -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 = "<group>"; };
0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyMainModel.h; sourceTree = "<group>"; };
0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyMainModel.m; sourceTree = "<group>"; };
0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WJXEventSource.h; sourceTree = "<group>"; };
0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WJXEventSource.m; sourceTree = "<group>"; };
049FB2092EC1C13800FAB05D /* KBSkinBottomActionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinBottomActionView.h; sourceTree = "<group>"; };
049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinBottomActionView.m; sourceTree = "<group>"; };
049FB20C2EC1CD2800FAB05D /* KBAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAlert.h; sourceTree = "<group>"; };
@@ -909,6 +912,15 @@
path = VM;
sourceTree = "<group>";
};
0498BDD92EE7ECEA006CC1D5 /* WJXEventSource */ = {
isa = PBXGroup;
children = (
0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */,
0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */,
);
path = WJXEventSource;
sourceTree = "<group>";
};
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 */,