Skip to content

Commit

Permalink
Revert "feat: implement GraphQL subscriptions (#1256)"
Browse files Browse the repository at this point in the history
This reverts commit 0a341f1.
  • Loading branch information
nedsalk committed Oct 10, 2023
1 parent f3c3817 commit afe9ef4
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 541 deletions.
6 changes: 0 additions & 6 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ module.exports = {
// Disable error on devDependencies importing since this isn't a TS library
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'no-await-in-loop': 0,
'no-restricted-syntax': [
'off',
{
selector: 'ForOfStatement',
},
],
'prefer-destructuring': 0,
'no-bitwise': 0,
'no-underscore-dangle': 'off',
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/ci-setup/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: "CI setup"
inputs:
node-version:
description: "Node version"
default: 18.18.0
default: 18.14.1
pnpm-version:
description: "PNPM version"
default: 8.6.1
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.18.0
18.14.1
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"author": "Fuel Labs <[email protected]> (https://fuel.network/)",
"private": true,
"engines": {
"node": ">= 18.18.0",
"pnpm": ">= 8.6.1"
"node": ">= 18.14.1",
"pnpm": ">= 8.1.1"
},
"packageManager": "pnpm@8.6.1",
"packageManager": "pnpm@8.1.1",
"scripts": {
"dev": "nodemon --config nodemon.config.json -x 'pnpm build:packages'",
"build": "turbo run build",
Expand Down Expand Up @@ -88,5 +88,10 @@
"tsx": "^3.12.7",
"turbo": "^1.8.8",
"typescript": "~5.1.6"
},
"pnpm": {
"overrides": {
"cross-fetch": "4.0.0"
}
}
}
5 changes: 1 addition & 4 deletions packages/errors/src/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export enum ErrorCode {
TYPE_NOT_FOUND = 'type-not-found',
TYPE_NOT_SUPPORTED = 'type-not-supported',
INVALID_DECODE_VALUE = 'invalid-decode-value',
JSON_ABI_ERROR = 'json-abi-error',
JSON_ABI_ERROR = 'abi-main-method-missing',
TYPE_ID_NOT_FOUND = 'type-id-not-found',
BIN_FILE_NOT_FOUND = 'bin-file-not-found',
CODER_NOT_FOUND = 'coder-not-found',
Expand Down Expand Up @@ -87,9 +87,6 @@ export enum ErrorCode {
SCRIPT_REVERTED = 'script-reverted',
SCRIPT_RETURN_INVALID_TYPE = 'script-return-invalid-type',

// general
FUEL_NODE_ERROR = 'fuel-node-error',

// coder
// ...
}
12 changes: 3 additions & 9 deletions packages/providers/codegen.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
{
"$schema": "https://www.graphql-code-generator.com/config.schema.json",
"schema": "./fuel-core-schema.graphql",
"documents": "./src/operations.graphql",
"generates": {
"./src/__generated__/operations.ts": {
"documents": "./src/operations.graphql",
"plugins": [
{ "typescript": {} },
{
"typescript-operations": {}
},
{
"typescript-generic-sdk": {
"dedupeFragments": true
}
}
{ "typescript-operations": {} },
{ "typescript-graphql-request": {} }
],
"config": {
"scalars": {
Expand Down
5 changes: 2 additions & 3 deletions packages/providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"graphql": "^16.6.0",
"graphql-request": "^5.0.0",
"graphql-tag": "^2.12.6",
"graphql-sse": "^2.2.1",
"ramda": "^0.29.0",
"tai64": "^1.0.0"
},
Expand All @@ -49,8 +48,8 @@
"@graphql-codegen/typescript": "^2.8.0",
"@graphql-codegen/typescript-graphql-request": "^4.5.7",
"@graphql-codegen/typescript-operations": "^2.5.5",
"@graphql-codegen/typescript-generic-sdk": "^3.1.0",
"@types/ramda": "^0.29.3",
"get-graphql-schema": "^2.1.2"
"get-graphql-schema": "^2.1.2",
"typescript": "^4.8.4"
}
}
59 changes: 21 additions & 38 deletions packages/providers/src/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,6 @@
# generate `operations.ts` from this file.

# Fragments

fragment transactionStatusFragment on TransactionStatus {
type: __typename
... on SubmittedStatus {
time
}
... on SuccessStatus {
block {
id
}
time
programState {
returnType
data
}
}
... on FailureStatus {
block {
id
}
time
reason
}
}

fragment transactionFragment on Transaction {
id
rawPayload
Expand All @@ -37,7 +12,27 @@ fragment transactionFragment on Transaction {
...receiptFragment
}
status {
...transactionStatusFragment
type: __typename
... on SubmittedStatus {
time
}
... on SuccessStatus {
block {
id
}
time
programState {
returnType
data
}
}
... on FailureStatus {
block {
id
}
time
reason
}
}
}

Expand Down Expand Up @@ -473,15 +468,3 @@ mutation produceBlocks(
startTimestamp: $startTimestamp
)
}

subscription submitAndAwait($encodedTransaction: HexString!) {
submitAndAwait(tx: $encodedTransaction) {
...transactionStatusFragment
}
}

subscription statusChange($transactionId: TransactionId!) {
statusChange(id: $transactionId) {
...transactionStatusFragment
}
}
129 changes: 18 additions & 111 deletions packages/providers/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { BytesLike } from '@ethersproject/bytes';
import { arrayify, hexlify } from '@ethersproject/bytes';
import type { Network } from '@ethersproject/networks';
Expand All @@ -14,10 +15,7 @@ import {
TransactionCoder,
} from '@fuel-ts/transactions';
import { checkFuelCoreVersionCompatibility } from '@fuel-ts/versions';
import { print } from 'graphql';
import { GraphQLClient } from 'graphql-request';
import type { Client } from 'graphql-sse';
import { createClient } from 'graphql-sse';
import { clone } from 'ramda';

import { getSdk as getOperationsSdk } from './__generated__/operations';
Expand Down Expand Up @@ -193,19 +191,14 @@ export type FetchRequestOptions = {
body: string;
};

export type CustomFetch<R extends Response = Response> = (
url: string,
options: FetchRequestOptions,
providerOptions?: Partial<Omit<ProviderOptions<R>, 'fetch'>>
) => Promise<R>;
/*
* Provider initialization options
*/
export type ProviderOptions<FetchResponse extends Response = Response> = {
fetch: CustomFetch<FetchResponse> | undefined;
cacheUtxo: number | undefined;
timeout: number | undefined;
export type ProviderOptions = {
fetch?: (url: string, options: FetchRequestOptions) => Promise<unknown>;
cacheUtxo?: number;
};

/**
* Provider Call transaction params
*/
Expand All @@ -227,26 +220,8 @@ type NodeInfoCache = Record<string, NodeInfo>;
* A provider for connecting to a node
*/
export default class Provider {
operations!: ReturnType<typeof getOperationsSdk>;
#subscriptionClient!: Client;

operations: ReturnType<typeof getOperationsSdk>;
cache?: MemoryCache;
options: ProviderOptions = {
timeout: undefined,
cacheUtxo: undefined,
fetch: undefined,
};

private static getFetchFn(options: ProviderOptions) {
return options.fetch !== undefined
? options.fetch
: (url: string, request: FetchRequestOptions) =>
fetch(url, {
...request,
signal:
options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined,
});
}

static clearChainAndNodeCaches() {
Provider.nodeInfoCache = {};
Expand All @@ -267,19 +242,18 @@ export default class Provider {
protected constructor(
/** GraphQL endpoint of the Fuel node */
public url: string,
options: Partial<ProviderOptions> = {}
public options: ProviderOptions = {}
) {
this.options = { ...this.options, ...options };
this.createOperations();
this.cache = this.options.cacheUtxo ? new MemoryCache(this.options.cacheUtxo) : undefined;
this.operations = this.createOperations(url, options);
this.cache = options.cacheUtxo ? new MemoryCache(options.cacheUtxo) : undefined;
}

/**
* Creates a new instance of the Provider class. This is the recommended way to initialize a Provider.
* @param url - GraphQL endpoint of the Fuel node
* @param options - Additional options for the provider
*/
static async create(url: string, options: Partial<ProviderOptions> = {}) {
static async create(url: string, options: ProviderOptions = {}) {
const provider = new Provider(url, options);
await provider.fetchChainAndNodeInfo();
return provider;
Expand Down Expand Up @@ -334,7 +308,7 @@ export default class Provider {
*/
async connect(url: string) {
this.url = url;
this.createOperations();
this.operations = this.createOperations(url);
await this.fetchChainAndNodeInfo();
}

Expand Down Expand Up @@ -374,77 +348,10 @@ export default class Provider {
* @param options - Additional options for the provider
* @returns The operation SDK object
*/
private createOperations() {
const fetchFn = Provider.getFetchFn(this.options);
const gqlClient = new GraphQLClient(this.url, {
fetch: (nodeUrl: string, request: FetchRequestOptions) =>
fetchFn(nodeUrl, request, this.options),
});

if (this.#subscriptionClient) this.#subscriptionClient.dispose();
this.#subscriptionClient = Provider.createSubscriptionClient(this.url, fetchFn, this.options);

// @ts-expect-error This is due to this function being generic and us using multiple libraries. Its type is specified when calling a specific operation via provider.operations.xyz.
this.operations = getOperationsSdk((query, vars) => {
const isSubscription =
(query.definitions.find((x) => x.kind === 'OperationDefinition') as { operation: string })
?.operation === 'subscription';
if (isSubscription) {
return this.#subscriptionClient.iterate({
query: print(query),
variables: vars as Record<string, unknown>,
});
}

return gqlClient.request(query, vars);
});
}

private static createSubscriptionClient(
url: string,
fetchFn: ReturnType<typeof Provider.getFetchFn>,
options: ProviderOptions
) {
return createClient({
url: `${url}-sub`,
onMessage: (msg) => {
/*
This is the only place where I've managed to wedge in error throwing
without the error being converted to the graphql-sse library's NetworkError or being silently ignored.
These are errors returned from the node as a field of the `data` property with a 200 response code,
so they aren't treated as errors by the graphql-sse library.
This function (onMessage) gets called after a fetch but before message processing.
So the fetchFn below gets called first, the node returns errors, then this function is called.
The _isError property is added in the response processing in the fetchFn as a way to differentiate between errors and successful responses.
See here: https://github.com/enisdenjo/graphql-sse/blob/370ec133f8ca9c7b763a6ca0223c756a09169c59/src/client.ts#L872
*/
if ((msg.data as { _isError: boolean })._isError) {
throw new FuelError(ErrorCode.FUEL_NODE_ERROR, JSON.stringify(msg.data?.errors));
}
},
fetchFn: async (
subscriptionUrl: string,
request: FetchRequestOptions & { signal: AbortSignal }
) => Provider.adaptSubscriptionResponse(await fetchFn(subscriptionUrl, request, options)),
});
}

/**
The subscription response processing serves two purposes:
1. To add an `event` field which is mandated by the graphql-sse library (not by the SSE protocol)
(see [the library's protocol](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md))
2. To process the node's response because it's a different format to the types generated by graphql-codegen.
*/
private static async adaptSubscriptionResponse(originalResponse: Response): Promise<Response> {
const originalResponseText = await originalResponse.text();
const originalResponseData = JSON.parse(originalResponseText.split('data:')[1]);
const data = originalResponseData.data;
const errors = originalResponseData.errors;

let text = 'event:next';
text += `\ndata:${JSON.stringify(data ?? { _isError: true, errors })}`;
text += '\n\n';
return new Response(text, originalResponse);
private createOperations(url: string, options: ProviderOptions = {}) {
this.url = url;
const gqlClient = new GraphQLClient(url, options.fetch ? { fetch: options.fetch } : undefined);
return getOperationsSdk(gqlClient);
}

/**
Expand Down Expand Up @@ -794,7 +701,7 @@ export default class Provider {
filter: { owner: owner.toB256(), assetId: assetId && hexlify(assetId) },
});

const coins = result.coins.edges.map((edge) => edge.node);
const coins = result.coins.edges!.map((edge) => edge!.node!);

return coins.map((coin) => ({
id: coin.utxoId,
Expand Down Expand Up @@ -1062,7 +969,7 @@ export default class Provider {
filter: { owner: owner.toB256() },
});

const balances = result.balances.edges.map((edge) => edge.node);
const balances = result.balances.edges!.map((edge) => edge!.node!);

return balances.map((balance) => ({
assetId: balance.assetId,
Expand All @@ -1089,7 +996,7 @@ export default class Provider {
owner: address.toB256(),
});

const messages = result.messages.edges.map((edge) => edge.node);
const messages = result.messages.edges!.map((edge) => edge!.node!);

return messages.map((message) => ({
messageId: InputMessageCoder.getMessageId({
Expand Down
Loading

0 comments on commit afe9ef4

Please sign in to comment.