diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c4f79..9f224ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,29 @@ -# Changelog +## [Unreleased] - 2023-09-04 + +### Added +1. Channel Creation happening from JS layer +2. Reset Connection +3. Reset Connection State based on current state +4. UI log to debug release build + +## [1.0.5] - 2023-04-12 + +### Added + +LINT FIX + +## [1.0.4] - 2023-04-12 + +### Added + +Add option to configure keepalive + + +## [1.0.3] - 2023-04-12 + +### Added + +Reverted swift dependency for ios pods ## [1.0.0-6] - 2023-01-13 diff --git a/README.md b/README.md index dea66b5..68af55a 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ gRPC for react-native ## Installation ```sh -npm install @mitch528/react-native-grpc +npm install @krishnafkh/react-native-grpc ``` ## Usage ```ts -import { GrpcClient, GrpcMetadata } from '@mitch528/react-native-grpc'; +import { GrpcClient, GrpcMetadata } from '@krishnafkh/react-native-grpc'; GrpcClient.setHost('example.com'); diff --git a/android/src/main/java/com/reactnativegrpc/GrpcModule.java b/android/src/main/java/com/reactnativegrpc/GrpcModule.java index c0e50ea..8b0c8d7 100644 --- a/android/src/main/java/com/reactnativegrpc/GrpcModule.java +++ b/android/src/main/java/com/reactnativegrpc/GrpcModule.java @@ -1,8 +1,11 @@ package com.reactnativegrpc; import android.util.Base64; +import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.UiThread; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -19,6 +22,7 @@ import io.grpc.CallOptions; import io.grpc.ClientCall; +import io.grpc.ConnectivityState; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Metadata; @@ -37,6 +41,7 @@ public class GrpcModule extends ReactContextBaseJavaModule { private boolean keepAliveEnabled = false; private Integer keepAliveTime; private Integer keepAliveTimeout; + private boolean isUiLogEnabled = false; private ManagedChannel managedChannel = null; @@ -63,34 +68,29 @@ public void getIsInsecure(final Promise promise) { @ReactMethod public void setHost(String host) { this.host = host; - this.handleOptionsChanged(); } @ReactMethod public void setInsecure(boolean insecure) { this.isInsecure = insecure; - this.handleOptionsChanged(); } @ReactMethod - public void setCompression(Boolean enable, String compressorName, String limit) { + public void setCompression(Boolean enable, String compressorName) { this.withCompression = enable; this.compressorName = compressorName; - this.handleOptionsChanged(); } @ReactMethod public void setResponseSizeLimit(int limit) { this.responseSizeLimit = limit; - this.handleOptionsChanged(); } @ReactMethod - public void setKeepalive(boolean enabled, int time, int timeout) { + public void setKeepAlive(boolean enabled, int time, int timeout) { this.keepAliveEnabled = enabled; this.keepAliveTime = time; this.keepAliveTimeout = timeout; - this.handleOptionsChanged(); } @ReactMethod @@ -326,15 +326,14 @@ private static String normalizePath(String path) { if (path.startsWith("/")) { path = path.substring(1); } - return path; } - private void handleOptionsChanged() { + @ReactMethod + public void initGrpcChannel() { if (this.managedChannel != null) { this.managedChannel.shutdown(); } - this.managedChannel = createManagedChannel(); } @@ -353,10 +352,66 @@ private ManagedChannel createManagedChannel() { channelBuilder = channelBuilder .keepAliveWithoutCalls(true) .keepAliveTime(keepAliveTime, TimeUnit.SECONDS) - .keepAliveTimeout(keepAliveTime, TimeUnit.SECONDS); + .keepAliveTimeout(keepAliveTimeout, TimeUnit.SECONDS); } managedChannel = channelBuilder.build(); return managedChannel; } + + @ReactMethod + public void resetConnection(final String message){ + if(null == managedChannel) return; + + this.managedChannel.resetConnectBackoff(); + + this.initGrpcChannel(); + + showToast("resetConnection "+message); + } + + @ReactMethod + public void onConnectionStateChange(){ + if(null == managedChannel) return; + + final ConnectivityState connectivityState = managedChannel.getState(true); + if(ConnectivityState.CONNECTING == connectivityState){ + showToast("onConnectionState CONNECTING"); + } else if(ConnectivityState.IDLE == connectivityState){ + showToast("onConnectionState IDLE"); + } else if(ConnectivityState.READY == connectivityState){ + showToast("onConnectionState READY"); + } else if(ConnectivityState.TRANSIENT_FAILURE == connectivityState){ + showToast("onConnectionState TRANSIENT_FAILURE"); + } else if(ConnectivityState.SHUTDOWN == connectivityState){ + showToast("onConnectionState SHUTDOWN"); + } else { + showToast("onConnectionState UNDEFINED"); + } + if(ConnectivityState.TRANSIENT_FAILURE == connectivityState && managedChannel.isTerminated() || managedChannel.isShutdown()){ + resetConnection("onConnectionStateChange"); + } + } + + @ReactMethod + public void enterIdle(){ + if(null == managedChannel) return; + + managedChannel.enterIdle(); + + showToast("enterIdle"); + } + + @ReactMethod + public void setUiLogEnabled(boolean isUiLogEnabled){ + this.isUiLogEnabled = isUiLogEnabled; + } + + @UiThread + private void showToast(final String message){ + if(!isUiLogEnabled || null == context) return; + + Toast.makeText(context,message,Toast.LENGTH_SHORT).show(); + Log.d("GRPC_MODULE",message); + } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6db48da..4645840 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -259,7 +259,7 @@ PODS: - React-jsinspector (0.70.6) - React-logger (0.70.6): - glog - - react-native-grpc (1.0.0-1): + - react-native-grpc (1.0.2): - gRPC-Swift - React-Core - React-perflogger (0.70.6) @@ -637,7 +637,7 @@ SPEC CHECKSUMS: React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0 - react-native-grpc: 328ef8ca6228c1a408dc0ebe3d0d5eab949fd13a + react-native-grpc: 9cfbb1f3064b90afaad667ee7a517cd37e7e9e66 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 React-RCTActionSheet: 7316773acabb374642b926c19aef1c115df5c466 React-RCTAnimation: 5341e288375451297057391227f691d9b2326c3d diff --git a/example/src/App.tsx b/example/src/App.tsx index f8a1250..c29c5ec 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,5 +1,5 @@ import 'text-encoding'; -import { GrpcClient } from '@mitch528/react-native-grpc'; +import { GrpcClient } from '@krishnafkh/react-native-grpc'; import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { RNGrpcTransport } from './transport'; diff --git a/example/src/transport.ts b/example/src/transport.ts index b08c1a7..7af8db6 100644 --- a/example/src/transport.ts +++ b/example/src/transport.ts @@ -1,6 +1,6 @@ /* eslint-disable eslint-comments/no-unlimited-disable */ import { ClientStreamingCall, DuplexStreamingCall, mergeRpcOptions, MethodInfo, RpcOptions, RpcOutputStreamController, RpcStatus, RpcTransport, ServerStreamingCall, UnaryCall } from '@protobuf-ts/runtime-rpc'; -import { GrpcClient, GrpcMetadata } from '@mitch528/react-native-grpc'; +import { GrpcClient, GrpcMetadata } from '@krishnafkh/react-native-grpc'; import { AbortSignal } from 'abort-controller'; /* eslint-disable */ @@ -32,11 +32,11 @@ export class RNGrpcTransport implements RpcTransport { }); } - const response = call.response.then(resp => method.O.fromBinary(resp)); + const response = call.response.then((resp: any) => method.O.fromBinary(resp)); const status = call.trailers.then(() => ({ code: 0, detail: '', - } as any), ({ error, code }) => ({ + } as any), ({ error, code }: any) => ({ code: code, detail: error })); @@ -52,14 +52,14 @@ export class RNGrpcTransport implements RpcTransport { const status = call.trailers.then(() => ({ code: 0, detail: '', - } as any), ({ error, code }) => ({ + } as any), ({ error, code }: any) => ({ code: code, detail: error })); const outStream = new RpcOutputStreamController(); - call.responses.on('data', (data) => { + call.responses.on('data', (data: any) => { outStream.notifyMessage(method.O.fromBinary(data)); }); @@ -69,7 +69,7 @@ export class RNGrpcTransport implements RpcTransport { } }); - call.responses.on('error', (reason) => { + call.responses.on('error', (reason: any) => { outStream.notifyError(reason); }); diff --git a/ios/ByteBuffer+GRPCPayload.swift b/ios/ByteBuffer+GRPCPayload.swift deleted file mode 100644 index 1415e91..0000000 --- a/ios/ByteBuffer+GRPCPayload.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Created by Mitchell Kutchuk on 11/14/22. -// - -import Foundation -import NIO -import GRPC - -extension ByteBuffer: GRPCPayload { - public init(serializedByteBuffer: inout ByteBuffer) { - self = serializedByteBuffer - } - - public func serialize(into buffer: inout ByteBuffer) { - var copy = self - buffer.writeBuffer(©) - } -} \ No newline at end of file diff --git a/ios/Grpc.h b/ios/Grpc.h index e06b7b1..b91e0c9 100644 --- a/ios/Grpc.h +++ b/ios/Grpc.h @@ -1,5 +1,10 @@ #import +#import -@interface Grpc : NSObject +@interface Grpc : RCTEventEmitter -@end +@property (nonatomic, copy) NSString* grpcHost; +@property (nonatomic, copy) NSNumber* grpcResponseSizeLimit; +@property (nonatomic, assign) BOOL grpcInsecure; + +@end \ No newline at end of file diff --git a/ios/Grpc.m b/ios/Grpc.m index 4fc94f6..209dbb2 100644 --- a/ios/Grpc.m +++ b/ios/Grpc.m @@ -1,52 +1,325 @@ -#import "React/RCTBridgeModule.h" -#import "RCTEventEmitter.h" +#import "Grpc.h" +#import +#import -@interface RCT_EXTERN_MODULE(Grpc, RCTEventEmitter) +@interface GrpcResponseHandler : NSObject -RCT_EXTERN_METHOD(setInsecure: - (nonnull NSNumber *)insecure) +- (instancetype)initWithInitialMetadataCallback:(void (^)(NSDictionary *))initialMetadataCallback + messageCallback:(void (^)(id))messageCallback + closeCallback:(void (^)(NSDictionary *, NSError *))closeCallback + writeDataCallback:(void (^)(void))writeDataCallback; -RCT_EXTERN_METHOD(setResponseSizeLimit: - (nonnull NSNumber *)limit) +- (instancetype)initWithInitialMetadataCallback:(void (^)(NSDictionary *))initialMetadataCallback + messageCallback:(void (^)(id))messageCallback + closeCallback:(void (^)(NSDictionary *, NSError *))closeCallback; -RCT_EXTERN_METHOD(setHost: - (NSString *) host) +@end -RCT_EXTERN_METHOD(setCompression: - (nonnull NSNumber *) enabled - compressorName: (NSString *) compressorName - limit: (NSString *) limit) +@implementation GrpcResponseHandler { + void (^_initialMetadataCallback)(NSDictionary *); -RCT_EXTERN_METHOD(setKeepalive: -(nonnull NSNumber *) enabled - time: (nonnull NSNumber *) time - timeout: (nonnull NSNumber *) timeout -) + void (^_messageCallback)(id); -RCT_EXTERN_METHOD(getIsInsecure: - (RCTPromiseResolveBlock) resolve reject: - (RCTPromiseRejectBlock) reject) + void (^_closeCallback)(NSDictionary *, NSError *); -RCT_EXTERN_METHOD(getResponseSizeLimit: - (RCTPromiseResolveBlock) resolve reject: - (RCTPromiseRejectBlock) reject) + void (^_writeDataCallback)(void); -RCT_EXTERN_METHOD(getHost: - (RCTPromiseResolveBlock) resolve reject: - (RCTPromiseRejectBlock) reject) + dispatch_queue_t _dispatchQueue; +} -RCT_EXTERN_METHOD(unaryCall: - (nonnull NSNumber *) callId path: (NSString*) path obj: (NSDictionary*) obj headers:(NSDictionary*) headers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +- (instancetype)initWithInitialMetadataCallback:(void (^)(NSDictionary *))initialMetadataCallback + messageCallback:(void (^)(id))messageCallback + closeCallback:(void (^)(NSDictionary *, NSError *))closeCallback + writeDataCallback:(void (^)(void))writeDataCallback { + if ((self = [super init])) { + _initialMetadataCallback = initialMetadataCallback; + _messageCallback = messageCallback; + _closeCallback = closeCallback; + _writeDataCallback = writeDataCallback; + _dispatchQueue = dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL); + } + return self; +} -RCT_EXTERN_METHOD(serverStreamingCall: - (nonnull NSNumber *) callId path: (NSString*) path obj: (NSDictionary*) obj headers:(NSDictionary*) headers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +- (instancetype)initWithInitialMetadataCallback:(void (^)(NSDictionary *))initialMetadataCallback + messageCallback:(void (^)(id))messageCallback + closeCallback:(void (^)(NSDictionary *, NSError *))closeCallback { + return [self initWithInitialMetadataCallback:initialMetadataCallback + messageCallback:messageCallback + closeCallback:closeCallback + writeDataCallback:nil]; +} -RCT_EXTERN_METHOD(clientStreamingCall: - (nonnull NSNumber *) callId path: (NSString*) path obj: (NSDictionary*) obj headers:(NSDictionary*) headers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +- (void)didReceiveInitialMetadata:(NSDictionary *)initialMetadata { + if (self->_initialMetadataCallback) { + self->_initialMetadataCallback(initialMetadata); + } +} -RCT_EXTERN_METHOD(finishClientStreaming: - (nonnull NSNumber *) callId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +- (void)didReceiveRawMessage:(id)message { + if (self->_messageCallback) { + self->_messageCallback(message); + } +} + +- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error { + if (self->_closeCallback) { + self->_closeCallback(trailingMetadata, error); + } +} + +- (void)didWriteData { + if (self->_writeDataCallback) { + self->_writeDataCallback(); + } +} + +- (dispatch_queue_t)dispatchQueue { + return _dispatchQueue; +} + +@end + +@implementation Grpc { + bool hasListeners; + NSMutableDictionary *calls; +} + +- (instancetype)init { + if (self = [super init]) { + calls = [[NSMutableDictionary alloc] init]; + } + + return self; +} + +// Will be called when this module's first listener is added. +- (void)startObserving { + hasListeners = YES; + // Set up any upstream listeners or background tasks as necessary +} + +// Will be called when this module's last listener is removed, or on dealloc. +- (void)stopObserving { + hasListeners = NO; + // Remove upstream listeners, stop unnecessary background tasks +} + +- (NSArray *)supportedEvents { + return @[@"grpc-call"]; +} + +- (GRPCCallOptions *)getCallOptionsWithHeaders:(NSDictionary *)headers { + GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init]; + options.initialMetadata = headers; + options.transport = self.grpcInsecure ? GRPCDefaultTransportImplList.core_insecure : GRPCDefaultTransportImplList.core_secure; + + if (self.grpcResponseSizeLimit != nil) { + options.responseSizeLimit = self.grpcResponseSizeLimit.unsignedLongValue; + } + + return options; +} + +RCT_EXPORT_METHOD(getHost: + (RCTPromiseResolveBlock) resolve) { + resolve(self.grpcHost); +} + +RCT_EXPORT_METHOD(getIsInsecure: + (RCTPromiseResolveBlock) resolve) { + resolve([NSNumber numberWithBool:self.grpcInsecure]); +} + +RCT_EXPORT_METHOD(setHost: + (NSString *) host) { + self.grpcHost = host; +} + + +RCT_EXPORT_METHOD(setInsecure: + (nonnull NSNumber*) insecure) { + self.grpcInsecure = [insecure boolValue]; +} + +RCT_EXPORT_METHOD(setResponseSizeLimit: + (nonnull NSNumber*) limit) { + self.grpcResponseSizeLimit = limit; +} + +RCT_EXPORT_METHOD(unaryCall: + (nonnull NSNumber*)callId + path:(NSString*)path + obj:(NSDictionary*)obj + headers:(NSDictionary*)headers + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSData *requestData = [[NSData alloc] initWithBase64EncodedString:[obj valueForKey:@"data"] options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + GRPCCall2 *call = [self startGrpcCallWithId:callId path:path headers:headers]; + + [call writeData:requestData]; + [call finish]; + + [calls setObject:call forKey:callId]; + + resolve([NSNull null]); +} + +RCT_EXPORT_METHOD(serverStreamingCall: + (nonnull NSNumber*)callId + path:(NSString*)path + obj:(NSDictionary*)obj + headers:(NSDictionary*)headers + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSData *requestData = [[NSData alloc] initWithBase64EncodedString:[obj valueForKey:@"data"] options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + GRPCCall2 *call = [self startGrpcCallWithId:callId path:path headers:headers]; + + [call writeData:requestData]; + [call finish]; + + [calls setObject:call forKey:callId]; + + resolve([NSNull null]); +} + +RCT_EXPORT_METHOD(cancelGrpcCall: + (nonnull NSNumber*)callId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + GRPCCall2 *call = [calls objectForKey:callId]; + + if (call != nil) { + [call cancel]; + + resolve([NSNumber numberWithBool:true]); + } else { + resolve([NSNumber numberWithBool:false]); + } +} + +RCT_EXPORT_METHOD(clientStreamingCall: + (nonnull NSNumber*)callId + path:(NSString*)path + obj:(NSDictionary*)obj + headers:(NSDictionary*)headers + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSData *requestData = [[NSData alloc] initWithBase64EncodedString:[obj valueForKey:@"data"] options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + GRPCCall2 *call = [calls objectForKey:callId]; + + if (call == nil) { + call = [self startGrpcCallWithId:callId path:path headers:headers]; + + [calls setObject:call forKey:callId]; + } + + [call writeData:requestData]; + + resolve([NSNull null]); +} + +RCT_EXPORT_METHOD(finishClientStreaming: + (nonnull NSNumber*)callId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + GRPCCall2 *call = [calls objectForKey:callId]; + + if (call != nil) { + [call finish]; + + resolve([NSNumber numberWithBool:true]); + } else { + resolve([NSNumber numberWithBool:false]); + } +} + +- (GRPCCall2 *)startGrpcCallWithId:(NSNumber *)callId path:(NSString *)path headers:(NSDictionary *)headers { + GRPCRequestOptions *requestOptions = [[GRPCRequestOptions alloc] initWithHost:self.grpcHost + path:path + safety:GRPCCallSafetyDefault]; + + GRPCCallOptions *callOptions = [self getCallOptionsWithHeaders:headers]; + + GrpcResponseHandler *handler = [[GrpcResponseHandler alloc] initWithInitialMetadataCallback:^(NSDictionary *initialMetadata) { + if (self->hasListeners) { + NSDictionary *responseHeaders = [[NSMutableDictionary alloc] initWithDictionary:initialMetadata]; + + [responseHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *exit) { + if ([object isKindOfClass:[NSData class]]) { + [responseHeaders setValue:[object base64EncodedStringWithOptions:0] forKey:key]; + } + }]; + + NSDictionary *event = @{ + @"id": callId, + @"type": @"headers", + @"payload": responseHeaders, + }; + + [self sendEventWithName:@"grpc-call" body:event]; + } + } + messageCallback:^(id message) { + NSData *data = (NSData *) message; + + if (self->hasListeners) { + NSDictionary *event = @{ + @"id": callId, + @"type": @"response", + @"payload": [data base64EncodedStringWithOptions:nil], + }; + + [self sendEventWithName:@"grpc-call" body:event]; + } + } + closeCallback:^(NSDictionary *trailingMetadata, NSError *error) { + [calls removeObjectForKey:callId]; + + NSDictionary *responseTrailers = [[NSMutableDictionary alloc] initWithDictionary:trailingMetadata]; + + [responseTrailers enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *exit) { + if ([object isKindOfClass:[NSData class]]) { + [responseTrailers setValue:[object base64EncodedStringWithOptions:0] forKey:key]; + } + }]; + + if (self->hasListeners) { + if (error != nil) { + NSDictionary *event = @{ + @"id": callId, + @"type": @"error", + @"error": error.localizedDescription, + @"code": [NSNumber numberWithLong:error.code], + @"trailers": responseTrailers, + }; + + [self sendEventWithName:@"grpc-call" body:event]; + } else { + NSDictionary *event = @{ + @"id": callId, + @"type": @"trailers", + @"payload": responseTrailers, + }; + + [self sendEventWithName:@"grpc-call" body:event]; + } + } + } + ]; + + GRPCCall2 *call = [[GRPCCall2 alloc] initWithRequestOptions:requestOptions + responseHandler:handler + callOptions:callOptions]; + + [call start]; + + return call; +} + +RCT_EXPORT_MODULE() -RCT_EXTERN_METHOD(cancelGrpcCall: - (nonnull NSNumber *) callId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) @end \ No newline at end of file diff --git a/ios/Grpc.xcodeproj/project.pbxproj b/ios/Grpc.xcodeproj/project.pbxproj index 6bfe22e..1e62edd 100644 --- a/ios/Grpc.xcodeproj/project.pbxproj +++ b/ios/Grpc.xcodeproj/project.pbxproj @@ -21,11 +21,6 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libGrpc.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libGrpc.a; sourceTree = BUILT_PRODUCTS_DIR; }; B3E7B5891CC2AC0600A0062D /* Grpc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Grpc.m; sourceTree = ""; }; - EB245ED12945270800E49A04 /* Grpc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Grpc.h; sourceTree = ""; }; - EB88E79329295B440030506F /* RNGrpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNGrpc.swift; sourceTree = ""; }; - EB88E79529295B440030506F /* ByteBuffer+GRPCPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ByteBuffer+GRPCPayload.swift"; sourceTree = ""; }; - EB88E79929295B450030506F /* GrpcError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrpcError.swift; sourceTree = ""; }; - EB88E79F29295C6E0030506F /* react-native-grpc-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "react-native-grpc-Bridging-Header.h"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -50,11 +45,6 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( - EB245ED12945270800E49A04 /* Grpc.h */, - EB88E79F29295C6E0030506F /* react-native-grpc-Bridging-Header.h */, - EB88E79529295B440030506F /* ByteBuffer+GRPCPayload.swift */, - EB88E79929295B450030506F /* GrpcError.swift */, - EB88E79329295B440030506F /* RNGrpc.swift */, B3E7B5891CC2AC0600A0062D /* Grpc.m */, 134814211AA4EA7D00B7C361 /* Products */, ); @@ -224,14 +214,12 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../../React/**", "$(SRCROOT)/../../react-native/React/**", - "$(SRCROOT)/../node_modules/react-native/React/**", - "$(SRCROOT)/../../../../../ios/Pods/Headers/**", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = Grpc; SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = "react-native-grpc-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = "Grpc-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -245,14 +233,12 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../../React/**", "$(SRCROOT)/../../react-native/React/**", - "$(SRCROOT)/../node_modules/react-native/React/**", - "$(SRCROOT)/../../../../../ios/Pods/Headers/**", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = Grpc; SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = "react-native-grpc-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = "Grpc-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/ios/GrpcError.swift b/ios/GrpcError.swift deleted file mode 100644 index cd68b48..0000000 --- a/ios/GrpcError.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Mitchell Kutchuk on 11/14/22. -// - -import Foundation - -enum GrpcError: String, Error { - case invalidHost = "Host is invalid" - case invalidHeader = "Header value is invalid" - case invalidData = "Data is invalid" - case invalidCallId = "Call id is invalid" - case callIdTypeMismatch = "Call with id did not match call type" - case connectionFailure = "Connection failure" - case notImplemented = "Not implemented" -} \ No newline at end of file diff --git a/ios/RNGrpc.swift b/ios/RNGrpc.swift deleted file mode 100644 index 505c255..0000000 --- a/ios/RNGrpc.swift +++ /dev/null @@ -1,411 +0,0 @@ -// -// Grpc.swift -// react-native-grpc -// -// Created by Mitchell Kutchuk on 11/12/22. -// - -import Foundation -import GRPC -import NIO -import NIOHPACK - -typealias GrpcCall = any ClientCall - -@objc(Grpc) -class RNGrpc: RCTEventEmitter { - private let group = PlatformSupport.makeEventLoopGroup(loopCount: System.coreCount) - - var grpcInsecure = false - var grpcHost: String? - var grpcResponseSizeLimit: Int? - var grpcCompression: Bool? - var grpcCompressorName: String? - var grpcCompressionLimit: Int? - var grpcKeepaliveEnabled: Bool? - var grpcKeepaliveTime: Int64? - var grpcKeepaliveTimeout: Int64? - var calls = [Int: GrpcCall]() - var connection: GRPCChannel? - - deinit { - try! group.syncShutdownGracefully() - } - - @objc - public func setInsecure(_ insecure: NSNumber) { - self.grpcInsecure = insecure.boolValue - self.handleOptionsChange() - } - - @objc - public func setHost(_ host: String) { - self.grpcHost = host - self.handleOptionsChange() - } - - @objc - public func setCompression(_ enabled: NSNumber, compressorName: String, - limit: String?) { - self.grpcCompression = enabled.boolValue - self.grpcCompressorName = compressorName - self.grpcCompressionLimit = Int(limit ?? "") - self.handleOptionsChange() - } - - @objc - public func setKeepalive(_ enabled: NSNumber, time: NSNumber, timeout: NSNumber) { - self.grpcKeepaliveEnabled = enabled.boolValue - self.grpcKeepaliveTime = time.int64Value - self.grpcKeepaliveTimeout = timeout.int64Value - self.handleOptionsChange() - } - - @objc - public func setResponseSizeLimit(_ responseSizeLimit: NSNumber) { - self.grpcResponseSizeLimit = responseSizeLimit.intValue - self.handleOptionsChange() - } - - @objc - public func getResponseSizeLimit(_ resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - resolve(self.grpcResponseSizeLimit) - } - - @objc - public func getIsInsecure(_ resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - resolve(self.grpcInsecure) - } - - @objc - public func getHost(_ resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - resolve(self.grpcHost) - } - - @objc - override func constantsToExport() -> [AnyHashable: Any]! { - [:] - } - - @objc - override static func requiresMainQueueSetup() -> Bool { - false - } - - @objc - public func unaryCall(_ callId: NSNumber, - path: String, - obj: NSDictionary, - headers: NSDictionary, - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - do { - try self.startGrpcCallWithId(callId: callId.intValue, obj: obj, type: .unary, path: path, headers: headers) - - resolve(nil) - } catch { - reject("grpc", error.localizedDescription, error) - } - } - - @objc - public func serverStreamingCall(_ callId: NSNumber, - path: String, - obj: NSDictionary, - headers: NSDictionary, - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - do { - try self.startGrpcCallWithId(callId: callId.intValue, obj: obj, type: .serverStreaming, path: path, headers: headers) - - resolve(nil) - } catch { - reject("grpc", error.localizedDescription, error) - } - } - - @objc - public func clientStreamingCall(_ callId: NSNumber, - path: String, - obj: NSDictionary, - headers: NSDictionary, - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - do { - var call: GrpcCall? = self.calls[callId.intValue] - - if call == nil { - call = try self.startGrpcCallWithId(callId: callId.intValue, obj: obj, type: .clientStreaming, path: path, headers: headers) - } - - guard let clientCall = call as? ClientStreamingCall else { - throw GrpcError.callIdTypeMismatch - } - - guard let base64 = obj["data"] as? String, let data = Data(base64Encoded: base64) else { - throw GrpcError.invalidData - } - - let payload = ByteBuffer(data: data) - - clientCall.sendMessage(payload) - - resolve(true) - } catch { - reject("grpc", error.localizedDescription, error) - } - } - - @objc - public func finishClientStreaming(_ callId: NSNumber, - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - do { - guard let call = self.calls[callId.intValue] else { - throw GrpcError.invalidCallId - } - - guard let clientCall = call as? ClientStreamingCall else { - throw GrpcError.callIdTypeMismatch - } - - clientCall.sendEnd() - .whenComplete({ result in - resolve(nil) - }) - } catch { - reject("grpc", error.localizedDescription, error) - } - } - - @objc - public func cancelGrpcCall(_ callId: NSNumber, - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock) { - guard let call = self.calls[callId.intValue] else { - resolve(false) - - return - } - - call.cancel(promise: nil) - - resolve(true) - } - - - private func startGrpcCallWithId(callId: Int, - obj: NSDictionary, - type: GRPCCallType, - path: String, - headers: NSDictionary) throws -> GrpcCall { - guard let conn = self.connection else { - throw GrpcError.connectionFailure - } - - guard let base64 = obj["data"] as? String, let data = Data(base64Encoded: base64) else { - throw GrpcError.invalidData - } - - let payload = ByteBuffer(data: data) - - let headerDict = headers.allKeys.map { - (String(describing: $0), String(describing: headers[$0]!)) - } - - let options = self.getCallOptionsWithHeaders(headers: HPACKHeaders(headerDict)) - - var call: GrpcCall - - var headers = [String: String]() - var trailers = [String: String]() - - func dispatchEvent(event: NSDictionary) { - self.sendEvent(withName: "grpc-call", body: event) - } - - func handleResponseResult(result: Result) { - switch result { - case .success(let response): - let data = Data(buffer: response) - let event: NSDictionary = [ - "id": callId, - "type": "response", - "payload": data.base64EncodedString() - ] - - dispatchEvent(event: event) - case .failure(let error): - let status = error as? GRPCStatus - - let event: NSDictionary = [ - "id": callId, - "type": "error", - "code": status?.code.rawValue ?? -1, - "error": status?.message ?? status?.description ?? "", - "trailers": NSDictionary(dictionary: trailers) - ] - - dispatchEvent(event: event) - } - } - - func removeCall() { - DispatchQueue.main.async { - self.calls.removeValue(forKey: callId) - } - } - - switch type { - case .unary: - let unaryCall: UnaryCall = conn.makeUnaryCall(path: path, request: payload, callOptions: options) - call = unaryCall - - unaryCall.response.whenComplete { result in - handleResponseResult(result: result) - removeCall() - } - case .clientStreaming: - let clientStreaming: ClientStreamingCall = conn.makeClientStreamingCall(path: path, callOptions: options) - - call = clientStreaming - - clientStreaming.response.whenComplete({ result in - handleResponseResult(result: result) - removeCall() - }) - case .serverStreaming: - let serverStreaming: ServerStreamingCall = conn.makeServerStreamingCall(path: path, request: payload, callOptions: options, interceptors: [ClientInterceptor](), handler: { response in - let data = Data(buffer: response) - let event: NSDictionary = [ - "id": callId, - "type": "response", - "payload": data.base64EncodedString() - ] - - dispatchEvent(event: event) - }) - - call = serverStreaming - default: - throw GrpcError.notImplemented - } - - call.initialMetadata.whenSuccess { result in - for data in result { - headers[data.name] = data.value - } - - let event: NSDictionary = [ - "id": callId, - "type": "headers", - "payload": NSDictionary(dictionary: headers) - ] - - dispatchEvent(event: event) - } - - call.trailingMetadata.whenSuccess { result in - for data in result { - trailers[data.name] = data.value - } - - let event: NSDictionary = [ - "id": callId, - "type": "trailers", - "payload": NSDictionary(dictionary: trailers) - ] - - dispatchEvent(event: event) - } - - calls[callId] = call - - return call - } - - private func getCallOptionsWithHeaders(headers: HPACKHeaders) -> CallOptions { - var encoding: ClientMessageEncoding = .disabled - - if let enabled = self.grpcCompression, enabled { - let compressionAlgorithm: [CompressionAlgorithm] - let limit = self.grpcCompressionLimit ?? .max - - switch self.grpcCompressorName { - case "gzip": - compressionAlgorithm = [.gzip] - case "deflate": - compressionAlgorithm = [.deflate] - case "identity": - compressionAlgorithm = [.identity] - default: - compressionAlgorithm = CompressionAlgorithm.all - } - - encoding = ClientMessageEncoding.enabled( - .init(forRequests: compressionAlgorithm.first, - acceptableForResponses: compressionAlgorithm, - decompressionLimit: .absolute(limit) - ) - ) - } - - return CallOptions(customMetadata: headers, messageEncoding: encoding) - } - - private func handleOptionsChange() { - if let conn = self.connection { - let loop = self.group.next() - conn.closeGracefully(deadline: .distantFuture, promise: loop.makePromise()) - } - - self.connection = try? self.createConnection() - } - - private func createConnection() throws -> GRPCChannel? { - guard let host = self.grpcHost else { - throw GrpcError.invalidHost - } - - guard let url = URLComponents(string: "https://\(host)"), let host = url.host else { - throw GrpcError.invalidHost - } - - let port = url.port ?? (self.grpcInsecure ? 80 : 443) - - var config = GRPCChannelPool.Configuration.with( - target: .hostAndPort(host, port), - transportSecurity: self.grpcInsecure ? .plaintext : .tls(.makeClientDefault(compatibleWith: self.group)), - eventLoopGroup: self.group - ) - - if let maxReceiveSize = self.grpcResponseSizeLimit { - config.maximumReceiveMessageLength = maxReceiveSize - } - - if let enabled = grpcKeepaliveEnabled, enabled { - let interval = self.grpcKeepaliveTime != nil ? TimeAmount.seconds(self.grpcKeepaliveTime!) : TimeAmount.nanoseconds(.max) - - let timeout = TimeAmount.seconds(grpcKeepaliveTimeout ?? 20) - - let keepalive = ClientConnectionKeepalive( - interval: interval, - timeout: timeout, - permitWithoutCalls: true - ) - - config.keepalive = keepalive - } - - return try? GRPCChannelPool.with(configuration: config) - } - - - @objc - override func supportedEvents() -> [String] { - ["grpc-call"] - } -} diff --git a/ios/react-native-grpc-Bridging-Header.h b/ios/react-native-grpc-Bridging-Header.h deleted file mode 100644 index 8992b22..0000000 --- a/ios/react-native-grpc-Bridging-Header.h +++ /dev/null @@ -1,3 +0,0 @@ -#import -#import -#import diff --git a/package.json b/package.json index 75095fc..ae8b0c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@mitch528/react-native-grpc", - "version": "1.0.0-6", + "name": "@krishnafkh/react-native-grpc", + "version": "1.0.5", "description": "gRPC for react-native", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -26,6 +26,7 @@ "test": "jest", "typescript": "tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", + "lint:fix": "eslint '**/*.{js,ts,tsx}' --fix", "prepare": "bob build", "release": "release-it", "example": "yarn --cwd example", @@ -38,13 +39,13 @@ "android", "grpc" ], - "repository": "https://github.com/Mitch528/react-native-grpc", - "author": "Mitch528 (https://github.com/Mitch528)", + "repository": "https://github.com/krishnafkh/react-native-grpc", + "author": "Krishna (https://github.com/krishnafkh/)", "license": "MIT", "bugs": { - "url": "https://github.com/Mitch528/react-native-grpc/issues" + "url": "https://github.com/krishnafkh/react-native-grpc/issues" }, - "homepage": "https://github.com/Mitch528/react-native-grpc#readme", + "homepage": "https://github.com/krishnafkh/react-native-grpc#readme", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" diff --git a/react-native-grpc.podspec b/react-native-grpc.podspec index 1c59af1..5651bc7 100644 --- a/react-native-grpc.podspec +++ b/react-native-grpc.podspec @@ -11,16 +11,20 @@ Pod::Spec.new do |s| s.authors = package["author"] s.platforms = { :ios => "10.0" } - s.source = { :git => "https://github.com/Mitch528/react-native-grpc.git", :tag => "#{s.version}" } + s.source = { :git => "https://github.com/krishnafkh/react-native-grpc.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" - s.static_framework = true s.dependency "React-Core" - s.dependency "gRPC-Swift" + s.dependency "gRPC" # Pods directory corresponding to this app's Podfile, relative to the location of this podspec. pods_root = 'Pods' -end + + s.pod_target_xcconfig = { + # This is needed by all pods that depend on gRPC-RxLibrary: + 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', + } +end \ No newline at end of file diff --git a/scripts/bootstrap.js b/scripts/bootstrap.js index cdba731..25e043e 100644 --- a/scripts/bootstrap.js +++ b/scripts/bootstrap.js @@ -24,7 +24,7 @@ if (process.cwd() !== root || args.length) { const scriptsDest = path.join( root, - 'example/node_modules/@mitch528/react-native-grpc/scripts' + 'example/node_modules/@krishnafkh/react-native-grpc/scripts' ); if (!fs.existsSync(path.dirname(scriptsDest))) { diff --git a/src/client.ts b/src/client.ts index 8eb5d22..de20df9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,6 @@ import { AbortController, AbortSignal } from 'abort-controller'; import { fromByteArray, toByteArray } from 'base64-js'; -import { NativeEventEmitter, NativeModules } from 'react-native'; +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import { GrpcError } from './errors'; import { GrpcServerStreamingCall, @@ -18,9 +18,9 @@ type GrpcType = { getIsInsecure: () => Promise; setHost(host: string): void; setInsecure(insecure: boolean): void; - setCompression(enable: boolean, compressorName: string, limit?: string): void; - setKeepalive(enabled: boolean, time: number, timeout: number): void; + setCompression(enable: boolean, compressorName: string): void; setResponseSizeLimit(limitInBytes: number): void; + initGrpcChannel(): void; unaryCall( id: number, path: string, @@ -41,6 +41,15 @@ type GrpcType = { requestHeaders?: GrpcMetadata ): Promise; finishClientStreaming(id: number): Promise; + resetConnection(message: string): void; + setKeepAlive( + enable: boolean, + keepAliveTime: number, + keepAliveTimeOut: number + ): void; + onConnectionStateChange(): void; + setUiLogEnabled(enable: boolean): void; + enterIdle(): void; }; type GrpcEventType = 'response' | 'error' | 'headers' | 'trailers'; @@ -51,20 +60,20 @@ type GrpcEventPayload = type: 'response'; payload: string; } | { - type: 'error'; - error: string; - code?: number; - trailers?: GrpcMetadata; - } | { - type: 'headers'; - payload: GrpcMetadata; - } | { - type: 'trailers'; - payload: GrpcMetadata; - } | { - type: 'status'; - payload: number; - }; + type: 'error'; + error: string; + code?: number; + trailers?: GrpcMetadata; +} | { + type: 'headers'; + payload: GrpcMetadata; +} | { + type: 'trailers'; + payload: GrpcMetadata; +} | { + type: 'status'; + payload: number; +}; type GrpcEvent = { id: number; @@ -140,13 +149,18 @@ function handleGrpcEvent(event: GrpcEvent) { case 'trailers': deferred.trailers?.resolve(event.payload); deferred.data?.notifyComplete(); + + delete deferredMap[event.id]; break; case 'error': const error = new GrpcError(event.error, event.code, event.trailers); + deferred.headers?.reject(error); + deferred.trailers?.reject(error); deferred.response?.reject(error); deferred.data?.noitfyError(error); + delete deferredMap[event.id]; break; } } @@ -175,19 +189,44 @@ export class GrpcClient { setInsecure(insecure: boolean): void { Grpc.setInsecure(insecure); } - setCompression( + setCompression(enable: boolean, compressorName: string): void { + Grpc.setCompression(enable, compressorName); + } + setResponseSizeLimit(limitInBytes: number): void { + Grpc.setResponseSizeLimit(limitInBytes); + } + + initGrpcChannel() { + Grpc.initGrpcChannel(); + } + + setKeepAlive( enable: boolean, - compressorName: string, - limit?: number + keepAliveTime: number, + keepAliveTimeOut: number ): void { - Grpc.setCompression(enable, compressorName, limit?.toString()); + Grpc.setKeepAlive(enable, keepAliveTime, keepAliveTimeOut); } - setKeepalive(enabled: boolean, time: number, timeout: number): void { - Grpc.setKeepalive(enabled, time, timeout); + + resetConnection(message: string): void { + if (!this.isAndroid()) return; + Grpc.resetConnection(message); } - setResponseSizeLimit(limitInBytes: number): void { - Grpc.setResponseSizeLimit(limitInBytes); + setUiLogEnabled(enable: boolean): void { + if (!this.isAndroid()) return; + Grpc.setUiLogEnabled(enable); + } + + onConnectionStateChange(): void { + if (!this.isAndroid()) return; + Grpc.onConnectionStateChange(); } + + enterIdle(): void { + if (!this.isAndroid()) return; + Grpc.enterIdle(); + } + unaryCall( method: string, data: Uint8Array, @@ -281,6 +320,10 @@ export class GrpcClient { return call; } + + private isAndroid(): Boolean { + return Platform.OS === 'android'; + } } export { Grpc }; diff --git a/tsconfig.json b/tsconfig.json index 7cccb8f..990e9d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@mitch528/react-native-grpc": [ + "@krishnafkh/react-native-grpc": [ "./src/index" ] },