Skip to content

Commit

Permalink
Further non-standard environment compatibility fixes/patches/improvem…
Browse files Browse the repository at this point in the history
…ents (#45)

Made client default to fetch if fetch-h2 fails and it isn't forced to be the default client

Allow users to pass in a custom fetcher

Examples folder

Further documentation

More tests
  • Loading branch information
toptobes authored May 29, 2024
1 parent f7d87e4 commit 5ef710a
Show file tree
Hide file tree
Showing 58 changed files with 2,749 additions and 253 deletions.
73 changes: 61 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
- [Working with Dates](#working-with-dates)
- [Working with ObjectIds and UUIDs](#working-with-objectids-and-uuids)
- [Monitoring/logging](#monitoringlogging)
- [Non-standard runtime support](#non-standard-runtime-support)
- [Non-standard environment support](#non-standard-environment-support)
- [HTTP/2 with minification](#http2-with-minification)
- [Browser support](#browser-support)

## Quickstart

Expand Down Expand Up @@ -331,30 +333,77 @@ client.on('commandFailed', (event) => {
})();
```

## Non-standard runtime support
## Non-standard environment support

`astra-db-ts` is designed foremost to work in Node.js environments, and it's not supported in browsers. It will work
in edge runtimes and other non-node environments as well, though it'll use the native `fetch` API for HTTP requests,
as opposed to `fetch-h2` which provides extended HTTP/2 and HTTP/1.1 support for performance.
`astra-db-ts` is designed foremost to work in Node.js environments.

On Node (exactly node; not Bun or Deno with node compat on) environments, it'll use `fetch-h2` by default; on others,
it'll use `fetch`. You can explicitly set the fetch implementation when instantiating the client:
It will work in edge runtimes and other non-node environments as well, though it'll use the native `fetch` API for HTTP
requests, as opposed to `fetch-h2` which provides extended HTTP/2 and HTTP/1.1 support for performance.

By default, it'll attempt to use `fetch-h2` if available, and fall back to `fetch` if not available in that environment.
You can explicitly force the fetch implementation when instantiating the client:

```typescript
import { DataAPIClient } from '@datastax/astra-db-ts';

const client = new DataAPIClient('*TOKEN*', {
httpOptions: {
client: 'fetch', // or 'default' for fetch-h2
},
httpOptions: { client: 'fetch' },
});
```

Note that setting the `httpOptions` implicitly sets the fetch implementation to default (fetch-h2),
so you'll need to set it explicitly if you want to use the native fetch implementation.
There are four different behaviours for setting the client:
- Not setting the `httpOptions` at all
- This will attempt to use `fetch-h2` if available, and fall back to `fetch` if not available
- `client: 'default'` or `client: undefined` (or unset)
- This will attempt to use `fetch-h2` if available, and throw an error if not available
- `client: 'fetch'`
- This will always use the native `fetch` API
- `client: 'custom'`
- This will allow you to pass a custom `Fetcher` implementation to the client

On some environments, such as Cloudflare Workers, you may additionally need to use the events
polyfill for the client to work properly (i.e. `npm i events`). Cloudflare's node-compat won't
work here.

Check out the `examples/` subdirectory for some non-standard runtime examples with more info.

### HTTP/2 with minification

Due to the variety of different runtimes JS can run in, `astra-db-ts` does its best to be as flexible as possible.
Unfortunately however, because we need to dynamically require the `fetch-h2` module to test whether it works, the
dynamic import often breaks in minified environments, even if the runtime properly supports HTTP/2.

There is a simple workaround however, consisting of the following steps, if you really want to use HTTP/2:
1. Install `fetch-h2` as a dependency (`npm i fetch-h2`)
2. Import the `fetch-h2` module in your code as `fetchH2` (i.e. `import * as fetchH2 from 'fetch-h2'`)
3. Set the `httpOptions.fetchH2` option to the imported module when instantiating the client

```typescript
import { DataAPIClient } from '@datastax/astra-db-ts';
import * as fetchH2 from 'fetch-h2';

const client = new DataAPIClient('*TOKEN*', {
httpOptions: { fetchH2 },
});
```

This way, the dynamic import is avoided, and the client will work in minified environments.

Note this is not required if you don't explicitly need HTTP/2 support, as the client will default
to the native fetch implementation instead if importing fails.

(But keep in mind this defaulting will only happen if `httpOptions` is not set at all).

(See `examples/http2-when-minified` for a full example of this workaround in action.)

### Browser support

The Data API itself does not natively support browsers, so `astra-db-ts` isn't technically supported in browsers either.

However, if, for some reason, you really want to use this in a browser, you can probably do so by installing the
`events` polyfill and setting up a [CORS proxy](https://github.com/Rob--W/cors-anywhere) to forward requests to the Data API.

But keep in mind that this is not officially supported, and may be very insecure if you're encoding sensitive
data into the browser client.

(See `examples/browser` for a full example of this workaround in action.)
57 changes: 43 additions & 14 deletions etc/astra-db-ts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,8 @@ export abstract class CumulativeDataAPIError extends DataAPIResponseError {
readonly partialResult: unknown;
}

// @public
export interface CuratedAPIResponse {
body?: string;
headers: Record<string, any>;
httpVersion: 1 | 2;
status: number;
statusText: string;
url: string;
}
// @public @deprecated
export type CuratedAPIResponse = FetcherResponseInfo;

// @public
export type CurrentDate<Schema> = {
Expand Down Expand Up @@ -381,8 +374,10 @@ export interface DataAPIErrorDescriptor {
readonly message?: string;
}

// Warning: (ae-forgotten-export) The symbol "CustomHttpClientOptions" needs to be exported by the entry point index.d.ts
//
// @public
export type DataAPIHttpOptions = FetchHttpClientOptions | DefaultHttpClientOptions;
export type DataAPIHttpOptions = DefaultHttpClientOptions | FetchHttpClientOptions | CustomHttpClientOptions;

// @public
export class DataAPIResponseError extends DataAPIError {
Expand Down Expand Up @@ -531,6 +526,7 @@ export interface DbSpawnOptions {
// @public
export interface DefaultHttpClientOptions {
client?: 'default';
fetchH2?: any;
http1?: Http1Options;
maxTimeMS?: number;
preferHttp2?: boolean;
Expand Down Expand Up @@ -587,12 +583,10 @@ export interface DevOpsAPIErrorDescriptor {

// @public
export class DevOpsAPIResponseError extends DevOpsAPIError {
// Warning: (ae-forgotten-export) The symbol "ResponseInfo" needs to be exported by the entry point index.d.ts
//
// @internal
constructor(resp: ResponseInfo, data: Record<string, any> | undefined);
constructor(resp: FetcherResponseInfo, data: Record<string, any> | undefined);
readonly errors: DevOpsAPIErrorDescriptor[];
readonly raw: CuratedAPIResponse;
readonly raw: FetcherResponseInfo;
readonly status: number;
}

Expand All @@ -616,6 +610,41 @@ export class DevOpsUnexpectedStateError extends DevOpsAPIError {
export interface DropCollectionOptions extends WithTimeout, WithNamespace {
}

// @public
export class FailedToLoadDefaultClientError extends Error {
// @internal
constructor(rootCause: Error);
readonly rootCause: Error;
}

// @public
export interface Fetcher {
close?(): Promise<void>;
fetch(info: FetcherRequestInfo): Promise<FetcherResponseInfo>;
}

// @public
export interface FetcherRequestInfo {
body: string | undefined;
forceHttp1: boolean | undefined;
headers: Record<string, string>;
method: 'DELETE' | 'GET' | 'POST';
mkTimeoutError: () => Error;
timeout: number;
url: string;
}

// @public
export interface FetcherResponseInfo {
additionalAttributes?: Record<string, any>;
body?: string;
headers: Record<string, any>;
httpVersion: 1 | 2;
status: number;
statusText: string;
url: string;
}

// @public
export interface FetchHttpClientOptions {
client: 'fetch';
Expand Down
5 changes: 5 additions & 0 deletions examples/browser/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Your Astra DB token (AstraCS:...)
VITE_ASTRA_DB_TOKEN=

# Your Astra DB endpoint (https://<id>-<region>.apps.astra.datastax.com)
VITE_ASTRA_DB_ENDPOINT=
24 changes: 24 additions & 0 deletions examples/browser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
70 changes: 70 additions & 0 deletions examples/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# astra-db-ts with HTTP/2 in a Minified Project

## Overview

The Data API itself does not natively support browsers, so `astra-db-ts` isn't technically supported in browsers either.

However, if, for some reason, you really want to use this in a browser, you can probably do so by installing the
`events` polyfill and setting up a [CORS proxy](https://github.com/Rob--W/cors-anywhere) to forward requests to the Data API. If no `httpOptions` are
provided, it will, by default, use the native `fetch` API (as the default `fetch-h2` isn't supported in browsers).

This is a simple example of how we can interact with an Astra database in a browser environment. It will list out
all the collections in a given database.

Do keep in mind that this is not officially supported, and may be very insecure if you're encoding sensitive
data into the browser client.

Check out the [Non-standard environment support](../../README.md#non-standard-environment-support) section
in the main `README.md` for more information common to non-standard environments.

## Getting Started

### Prerequisites:

- Ensure you have an existing Astra Database running at [astra.datastax.com](https://astra.datastax.com/).
- You'll need an API key and a database endpoint URL to get started.

### How to Use This Example:

1. Clone this repository to your local machine.

2. Run `npm install` to install the required dependencies.

3. Copy the `.env.example` file to `.env` and fill in the required values.

4. Run `npm run dev` to start the local development server.

5. Visit `http://localhost:5173` in your browser to see the example in action.

### Steps to Start Your Own Project:

1. Create a new project as you please.

2. Install `@datastax/astra-db-ts` by running `npm i @datastax/astra-db-ts`.

3. Install the `events` polyfill (if your build tool doesn't provide polyfills) by running `npm i events`.

4. Set up a CORS proxy to forward requests to the Data API. You can use [cors-anywhere](https://github.com/Rob--W/cors-anywhere),
or any other CORS proxy of your choice.

5. When doing `client.db()`, prefix the endpoint URL with the CORS proxy URL as appropriate.

6. You should be able to use `@datastax/astra-db-ts` in your project as normal now.

**Please be very careful about not hard-coding credentials or sensitive data in your client-side code.**

## Full Code Sample

```ts
import { DataAPIClient } from '@datastax/astra-db-ts';

const client = new DataAPIClient(prompt('Enter your AstraDB API key: '));
const db = client.db(`${import.meta.env.CORS_PROXY_URL}${import.meta.env.ASTRA_DB_ENDPOINT}`);

const app = document.querySelector<HTMLDivElement>('#app')!;
app.innerHTML = '<p>Loading...</p>';

db.listCollections().then((collections) => {
app.innerHTML = `<code>${JSON.stringify(collections, null, 2)}</code>`;
});
```
13 changes: 13 additions & 0 deletions examples/browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Loading

0 comments on commit 5ef710a

Please sign in to comment.