Skip to content

Commit

Permalink
feat: add client for node-http(s)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhweiner committed Jan 29, 2024
1 parent b593c83 commit 4afe232
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 8 deletions.
112 changes: 112 additions & 0 deletions src/client-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/* eslint-disable max-lines-per-function */
import http from 'http';
import https from 'https';
import {URL} from 'url';
import {invokeOrFail} from './lib/invokeOrFail';
import toResult from './lib/toResult';

export type ClientConfig = {
endpoint: string
options?: https.RequestOptions
onError?: (error: any) => void
};

export class Non200Response<T> extends Error {

status: number;
response: T;

constructor(status: number, response: T) {

super('Non200Response');
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
this.name = 'Non200Response';
this.status = status;
this.response = response;

}

}

/**
* We're purposely not putting any type alias for the client route to aid in better IDE intellisense.
* Otherwise, the TS compiler/autocomplete might suggest the type alias instead of the underlying (initial) type.
*/
export async function client<A extends {
name: string
input: any
output: any
}, ErrorResponseType = {}>(
name: A['name'],
input: A['input'],
options?: ClientConfig
): Promise<A['output']> {

const url = new URL(`${options?.endpoint}/${name}`);
const requestOptions = {
hostname: url.hostname,
port: url.port,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options?.options?.headers,
},
protocol: url.protocol,
};

const [err, resp] = await toResult(httpRequestPromise(requestOptions, JSON.stringify(input || {})));

if (err) {

options?.onError?.(err);
throw err;

}

const responseData = await new Promise((resolve, reject) => {

let data = '';

resp.on('data', (chunk) => data += chunk);
resp.on('end', () => resolve(data));
resp.on('error', reject);

}) as string;

let parsedData: any;

if (responseData.trim().length) {

const [, result] = invokeOrFail(() => JSON.parse(responseData));

parsedData = result;

}

resp.statusCode = resp.statusCode || 0;

if (resp.statusCode < 200 || resp.statusCode >= 300) {

throw new Non200Response(resp.statusCode, parsedData as ErrorResponseType);

}

return parsedData as A['output'];

}

async function httpRequestPromise(options: https.RequestOptions, body: string): Promise<http.IncomingMessage> {

return new Promise((resolve, reject) => {

const req = options.protocol === 'https:' ? https.request(options, resolve) : http.request(options, resolve);

req.on('error', reject);
req.write(body);
req.end();

});

}

10 changes: 5 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines-per-function */
import {invokeOrFail} from './lib/invokeOrFail';
import toResult from './lib/toResult';

Expand All @@ -24,11 +25,10 @@ export class Non200Response<T> extends Error {

}


// We're purposely not putting any type alias for the client route to aid in better IDE intellisense.
// Otherwise, the TS compiler/autocomplete might suggest the type alias instead of the underlying (initial) type.

// eslint-disable-next-line max-lines-per-function
/**
* We're purposely not putting any type alias for the client route to aid in better IDE intellisense.
* Otherwise, the TS compiler/autocomplete might suggest the type alias instead of the underlying (initial) type.
*/
export async function client<A extends {
name: string
input: any
Expand Down
7 changes: 4 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
export type Resolver<I, O, C> = (input: I, context: C) => O;
export type ExpressContextResolver<C> = (req: Request) => C;

// We're purposely not using a type alias for the client route to aid in better IDE intellisense.
// Otherwise, the TS compiler/autocomplete might suggest the type alias instead of the underlying
// (initial) type.
/* We're purposely not using a type alias for the client route to aid in better IDE intellisense.
* Otherwise, the TS compiler/autocomplete might suggest the type alias instead of the underlying
* (initial) type.
*/

type InferResolver<N, R> = R extends Resolver<infer I, infer O, any> ? {
name: N
Expand Down

0 comments on commit 4afe232

Please sign in to comment.