Skip to content

Commit

Permalink
feat: add http2 support
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanosdev committed Apr 22, 2024
1 parent 2b6b925 commit 8425289
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 126 deletions.
6 changes: 3 additions & 3 deletions docs/docs/guides/03-using-vitest.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function teardown(): Promise<void> {

To improve type-safety for `ctx.provide('PIC_URL')` and (later) `inject('PIC_URL')`, create a `types.d.ts` file:

```typescript title="types.d.ts"
```ts title="types.d.ts"
export declare module 'vitest' {
export interface ProvidedContext {
PIC_URL: string;
Expand All @@ -101,7 +101,7 @@ export declare module 'vitest' {

Create a `vitest.config.ts` file:

```typescript title="vitest.config.ts"
```ts title="vitest.config.ts"
import { defineConfig } from 'vitest/config';

export default defineConfig({
Expand All @@ -119,7 +119,7 @@ export default defineConfig({

The basic skeleton of all PicJS tests written with [Vitest](https://vitest.dev/) will look something like this:

```typescript title="tests/example.spec.ts"
```ts title="tests/example.spec.ts"
import { describe, beforeEach, afterEach, it, expect, inject } from 'vitest';

// Import generated types for your canister
Expand Down
37 changes: 37 additions & 0 deletions docs/docs/guides/05-running-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Running tests

## Configuring logging

### Canister logs

Logs for canisters can be configured when running the PocketIC server using the `showCanisterLogs` option, for example:

```ts
const pic = await PocketIcServer.start({
showCanisterLogs: true,
});
```

### Server logs

Logs for the PocketIC server can be configured by setting the `POCKET_IC_LOG_DIR` and `POCKET_IC_LOG_DIR_LEVELS` environment variables.

The `POCKET_IC_LOG_DIR` variable specifies the directory where the logs will be stored. It accepts any valid relative, or absolute directory path.

The `POCKET_IC_LOG_DIR_LEVELS` variable specifies the log levels. It accepts any of the following `string` values: `trace`, `debug`, `info`, `warn`, or `error`.

For example:

```shell
POCKET_IC_LOG_DIR=./logs POCKET_IC_LOG_DIR_LEVELS=trace npm test
```

### Runtime logs

Logs for the IC runtime can be configured when running the PocketIC server using the `showRuntimeLogs` option, for example:

```ts
const pic = await PocketIcServer.start({
showRuntimeLogs: true,
});
```
File renamed without changes.
4 changes: 2 additions & 2 deletions examples/clock/tests/global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { PocketIcServer } from '@hadronous/pic';

module.exports = async function (): Promise<void> {
const pic = await PocketIcServer.start({
pipeStdout: false,
pipeStderr: true,
showCanisterLogs: true,
showRuntimeLogs: false,
});
const url = pic.getUrl();

Expand Down
2 changes: 1 addition & 1 deletion examples/nns_proxy/tests/src/nns-proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('NNS Proxy', () => {
throw new Error('NNS subnet not found');
}

const rootKey = pic.getPubKey(nnsSubnet.id);
const rootKey = await pic.getPubKey(nnsSubnet.id);
expect(rootKey).toBeDefined();
});
});
Expand Down
55 changes: 0 additions & 55 deletions packages/pic/src/http-client.ts

This file was deleted.

187 changes: 187 additions & 0 deletions packages/pic/src/http2-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import http2, {
ClientHttp2Session,
IncomingHttpHeaders,
OutgoingHttpHeaders,
} from 'node:http2';

const { HTTP2_HEADER_PATH, HTTP2_HEADER_METHOD } = http2.constants;

export interface Request {
method: Method;
path: string;
headers?: OutgoingHttpHeaders;
body?: Uint8Array;
}

export interface JsonGetRequest {
path: string;
headers?: OutgoingHttpHeaders;
}

export interface JsonPostRequest<B> {
path: string;
headers?: OutgoingHttpHeaders;
body?: B;
}

export interface Response {
status: number | undefined;
body: string;
headers: IncomingHttpHeaders;
}

export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';

export const JSON_HEADER: OutgoingHttpHeaders = {
'Content-Type': 'application/json',
};

export class Http2Client {
private readonly session: ClientHttp2Session;

constructor(baseUrl: string) {
this.session = http2.connect(baseUrl);
}

public request(init: Request): Promise<Response> {
return new Promise((resolve, reject) => {
let req = this.session.request({
[HTTP2_HEADER_PATH]: init.path,
[HTTP2_HEADER_METHOD]: init.method,
'content-length': init.body?.length ?? 0,
...init.headers,
});

req.on('error', error => {
console.error('Erorr sending request to PocketIC server', error);
return reject(error);
});

req.on('response', headers => {
const status = headers[':status'] ?? -1;

const contentLength = headers['content-length']
? Number(headers['content-length'])
: 0;
let buffer = Buffer.alloc(contentLength);
let bufferLength = 0;

req.on('data', (chunk: Buffer) => {
chunk.copy(buffer, bufferLength);
bufferLength += chunk.length;
});

req.on('end', () => {
const body = buffer.toString('utf8');

return resolve({
status,
body,
headers,
});
});
});

if (init.body) {
req.write(init.body, 'utf8');
}

req.end();
});
}

public async jsonGet<R extends {}>(init: JsonGetRequest): Promise<R> {
while (true) {
const res = await this.request({
method: 'GET',
path: init.path,
headers: { ...init.headers, ...JSON_HEADER },
});

const resBody = JSON.parse(res.body) as ApiResponse<R>;
if (!resBody) {
return resBody;
}

// server encountered an error
if ('message' in resBody) {
console.error('PocketIC server encountered an error', resBody.message);

throw new Error(resBody.message);
}

// the server has started processing or is busy
if ('state_label' in resBody) {
console.error('PocketIC server is too busy to process the request');

if (res.status === 202) {
throw new Error('Server started processing');
}

if (res.status === 409) {
throw new Error('Server busy');
}

throw new Error('Unknown state');
}

return resBody;
}
}

public async jsonPost<B, R>(init: JsonPostRequest<B>): Promise<R> {
const reqBody = init.body
? new TextEncoder().encode(JSON.stringify(init.body))
: undefined;

while (true) {
const res = await this.request({
method: 'POST',
path: init.path,
headers: { ...init.headers, ...JSON_HEADER },
body: reqBody,
});

const resBody = JSON.parse(res.body);
if (!resBody) {
return resBody;
}

// server encountered an error
if ('message' in resBody) {
console.error('PocketIC server encountered an error', resBody.message);

throw new Error(resBody.message);
}

// the server has started processing or is busy
// sleep and try again
if ('state_label' in resBody) {
console.error('PocketIC server is too busy to process the request');

if (res.status === 202) {
throw new Error('Server started processing');
}

if (res.status === 409) {
throw new Error('Server busy');
}

throw new Error('Unknown state');
}

return resBody;
}
}
}

interface StartedOrBusyApiResponse {
state_label: string;
op_id: string;
}

interface ErrorResponse {
message: string;
}

type ApiResponse<R extends {}> = StartedOrBusyApiResponse | ErrorResponse | R;
12 changes: 2 additions & 10 deletions packages/pic/src/pocket-ic-client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface CreateInstanceRequest {
bitcoin?: boolean;
system?: number;
application?: number;
processingTimeoutMs?: number;
}

export interface EncodedCreateInstanceRequest {
Expand Down Expand Up @@ -564,15 +565,10 @@ export interface EncodedCanisterCallErrorResponse {
};
}

export interface EncodedCanisterCallErrorMessageResponse {
message: string;
}

export type EncodedCanisterCallResponse =
| EncodedCanisterCallSuccessResponse
| EncodedCanisterCallRejectResponse
| EncodedCanisterCallErrorResponse
| EncodedCanisterCallErrorMessageResponse;
| EncodedCanisterCallErrorResponse;

export function decodeCanisterCallResponse(
res: EncodedCanisterCallResponse,
Expand All @@ -581,10 +577,6 @@ export function decodeCanisterCallResponse(
throw new Error(res.Err.description);
}

if ('message' in res) {
throw new Error(res.message);
}

if ('Reject' in res.Ok) {
throw new Error(res.Ok.Reject);
}
Expand Down
Loading

0 comments on commit 8425289

Please sign in to comment.