Skip to content

Commit

Permalink
feat: Fix react native support (#1382)
Browse files Browse the repository at this point in the history
  • Loading branch information
fundthmcalculus authored Apr 20, 2023
1 parent 4fe00d4 commit a13ba63
Show file tree
Hide file tree
Showing 18 changed files with 656 additions and 2,343 deletions.
19 changes: 8 additions & 11 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
## Installation

```bash
npm install @trinsic/sdk
npm install @trinsic/trinsic
```

```ts
import { TrinsicService } from "@trinsic/sdk";
import { TrinsicService } from "@trinsic/trinsic";
```

For bundlers that do not use the `"browser"` field, you can directly import the required web package with:

```ts
import { AccountService } from "@trinsic/sdk/browser";
If you have need to override the transport method (for instance, old versions of Node), you can do it in the following manner:
```typescript
import {TransportProvider} from "@trinsic/trinsic"
TransportProvider.overrideTransport = XHRTransport(); // or other `nice-grpc-web` transports
// Proceed to make your SDK calls here
```

## Documentation
Expand All @@ -29,17 +30,13 @@ Install the following requirements:
- Node.js
- [Powershell](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.1)
- `protoc` compiler for your platform - [installation info](https://grpc.io/docs/protoc-installation/)
- `grpc-web` plugin - see [this section](https://github.com/grpc/grpc-web#code-generator-plugin) for installation info

```sh
npm install -g grpc-web prettier
grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./dist --grpc_out=grpc_js:./dist --proto_path=../../proto ProviderService.proto
npm run build:proto
```

After this, you can run `npm ci` and `npm build`.

The build script will generate the proto files by running the `Generate-Proto.ps1` script. You can also run this script manually.

- We use [prettier](https://prettier.io/) for code formatting.
- Any test marked with `.spec.ts` is used by node AND browser. Any test marked with `.test.ts` is only used by node, and can have node-specific functionality.
- We use the environment variable `TEST_SERVER_NODE_PROTOCOL` to determine which communication protocol node uses for testing.
339 changes: 192 additions & 147 deletions web/package-lock.json

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@
],
"license": "ISC",
"dependencies": {
"@improbable-eng/grpc-web-node-http-transport": "0.15.0",
"@azure/core-asynciterator-polyfill": "1.0.2",
"buffer": "6.0.3",
"google-protobuf": "3.21.0",
"long": "5.2.0",
"nice-grpc-web": "3.2.3",
"js-base64": "3.7.2"
"fastestsmallesttextencoderdecoder": "1.0.22",
"google-protobuf": "3.21.2",
"js-base64": "3.7.5",
"long": "5.2.3",
"nice-grpc-web": "3.2.4",
"protobufjs": "7.2.3"
},
"devDependencies": {
"@babel/core": "7.18.10",
Expand Down
198 changes: 198 additions & 0 deletions web/src/FetchReactNativeTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import {throwIfAborted} from 'abort-controller-x';
import {ClientError, Metadata, Status} from 'nice-grpc-common';
import {Transport} from 'nice-grpc-web/src/client/Transport';
import {FetchTransportConfig} from "nice-grpc-web/lib/client/transports/fetch";
import {Base64} from "js-base64";

// @ts-ignore
// import {fetch} from "react-native-fetch-api";
// import { fetch } from "whatwg-fetch";
// import fetch from 'react-native-fetch-polyfill';

export function blobReaderAsync(myBlob: Blob): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onloadend = function () {
const dataString = (fileReader.result as string);
const base64String = dataString.substring(dataString.indexOf(",")+1);
const uint8Data = Base64.toUint8Array(base64String);
resolve(uint8Data);
}
fileReader.readAsDataURL(myBlob);
});
}

/**
* Transport for browsers based on `fetch` API.
*/
export function FetchReactNativeTransport(config?: FetchTransportConfig): Transport {
return async function* fetchTransport({url, body, metadata, signal, method}) {
let requestBody: BodyInit;

if (!method.requestStream) {
let bodyBuffer: Uint8Array | undefined;

for await (const chunk of body) {
bodyBuffer = chunk;

break;
}

requestBody = bodyBuffer!;
} else {
let iterator: AsyncIterator<Uint8Array> | undefined;

requestBody = new ReadableStream({
// @ts-ignore
type: 'bytes',
start() {
iterator = body[Symbol.asyncIterator]();
},

async pull(controller) {
const {done, value} = await iterator!.next();

if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
async cancel() {
await iterator!.return?.();
},
});
}

const response: Response = await fetch(url, {
method: 'POST',
body: requestBody,
headers: metadataToHeaders(metadata),
signal,
cache: 'no-cache',
['duplex' as any]: 'half',
credentials: config?.credentials,
});

yield {
type: 'header',
header: headersToMetadata(response.headers),
};

if (!response.ok) {
const responseText = await response.text();

throw new ClientError(
method.path,
getStatusFromHttpCode(response.status),
getErrorDetailsFromHttpResponse(response.status, responseText),
);
}

throwIfAborted(signal);

const dataBlob = await response.blob();
const myBlobArray = await blobReaderAsync(dataBlob);

try {
for (const uint8Array of [myBlobArray]) {
if (uint8Array !== null) {
yield {
type: 'data',
data: uint8Array,
};
}
}
} finally {
throwIfAborted(signal);
}
};
}

// Lifted from: https://github.com/improbable-eng/grpc-web/blob/master/client/grpc-web-react-native-transport/src/index.ts
function stringToArrayBuffer(str: string): Uint8Array {
const asArray = new Uint8Array(str.length);
let arrayIndex = 0;
for (let i = 0; i < str.length; i++) {
const codePoint = (String.prototype as any).codePointAt ? (str as any).codePointAt(i) : codePointAtPolyfill(str, i);
asArray[arrayIndex++] = codePoint & 0xFF;
}
return asArray;
}

function codePointAtPolyfill(str: string, index: number) {
let code = str.charCodeAt(index);
if (code >= 0xd800 && code <= 0xdbff) {
const surr = str.charCodeAt(index + 1);
if (surr >= 0xdc00 && surr <= 0xdfff) {
code = 0x10000 + ((code - 0xd800) << 10) + (surr - 0xdc00);
}
}
return code;
}

function metadataToHeaders(metadata: Metadata): Headers {
const headers = new Headers();

for (const [key, values] of metadata) {
for (const value of values) {
headers.append(
key,
typeof value === 'string' ? value : Base64.fromUint8Array(value, true),
);
}
}

return headers;
}

function headersToMetadata(headers: Headers): Metadata {
const metadata = new Metadata();

// @ts-ignore
for (const [key, value] of headers) {
if (key.endsWith('-bin')) {
for (const item of value.split(/,\s?/)) {
metadata.append(key, Base64.toUint8Array(item));
}
} else {
metadata.set(key, value);
}
}

return metadata;
}

function getStatusFromHttpCode(statusCode: number): Status {
switch (statusCode) {
case 200:
return Status.OK;
case 400:
return Status.INTERNAL;
case 401:
return Status.UNAUTHENTICATED;
case 403:
return Status.PERMISSION_DENIED;
case 404:
return Status.UNIMPLEMENTED;
case 429:
case 502:
case 503:
case 504:
return Status.UNAVAILABLE;
default:
return Status.UNKNOWN;
}
}

function getErrorDetailsFromHttpResponse(
statusCode: number,
responseText: string,
): string {
return (
`Received HTTP ${statusCode} response: ` +
(responseText.length > 1000
? responseText.slice(0, 1000) + '... (truncated)'
: responseText)
);
}
4 changes: 2 additions & 2 deletions web/src/ServiceBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
CompatServiceDefinition as ClientServiceDefinition,
} from "nice-grpc-web";
import { getSdkVersion } from "./Version";
import { BrowserProvider, IPlatformProvider } from "./providers";
import { TransportProvider, IPlatformProvider } from "./providers";

export default abstract class ServiceBase {
static platform: IPlatformProvider = new BrowserProvider();
static platform: IPlatformProvider = new TransportProvider();
options: TrinsicOptions;

protected constructor(
Expand Down
Loading

0 comments on commit a13ba63

Please sign in to comment.