Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add http2 support #43

Merged
merged 1 commit into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
});
```
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
Loading