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

chore: support RPC consistency mechanism #3741

Draft
wants to merge 44 commits into
base: ns/chore/fuel-core-0.42.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1da934c
wip
nedsalk Feb 26, 2025
7742b82
wip
nedsalk Feb 27, 2025
6f970b3
Merge branch 'st/chore/[email protected]' into optimistic-reba…
nedsalk Feb 27, 2025
0aa6ddc
dont handle errors for now
nedsalk Feb 27, 2025
2f01aa9
fix startingGasPrice issue
nedsalk Feb 27, 2025
db29926
wip
nedsalk Feb 27, 2025
6d31a34
remove .only
nedsalk Feb 27, 2025
534410b
going strong
nedsalk Feb 28, 2025
5958888
looks pretty good
nedsalk Feb 28, 2025
e7f4f82
undo change
nedsalk Feb 28, 2025
0ff1e0e
renamings
nedsalk Feb 28, 2025
a22d325
add test groups
nedsalk Feb 28, 2025
48fceec
improve
nedsalk Feb 28, 2025
ee665b6
fix tests
nedsalk Feb 28, 2025
49c4f0e
looks good
nedsalk Mar 3, 2025
e01088e
Merge branch 'st/chore/[email protected]' into ns/feat/optimis…
nedsalk Mar 3, 2025
802dfdb
simplify type
nedsalk Mar 3, 2025
282761b
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 4, 2025
06dc0f7
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 4, 2025
4eac762
clean cache on node kill
nedsalk Mar 4, 2025
339d130
remove `.only`
nedsalk Mar 4, 2025
45d50e7
improve comments
nedsalk Mar 4, 2025
97362d6
revert `deploy.test.ts` changes for now
nedsalk Mar 4, 2025
9f43b14
update changeset
nedsalk Mar 4, 2025
1e0a5e6
Update VERSION
nedsalk Mar 4, 2025
1d16d3a
normalize url
nedsalk Mar 4, 2025
4b7c01f
refactor block height addition into method
nedsalk Mar 4, 2025
4b7a29b
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 5, 2025
05bc7b9
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 6, 2025
cac250d
refactor retrying
nedsalk Mar 6, 2025
0a57286
fix preconfirm status precompilation issues
nedsalk Mar 6, 2025
ed74b07
wip
nedsalk Mar 7, 2025
2f513bc
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 10, 2025
2aa3ce5
rm unused
nedsalk Mar 10, 2025
ff41996
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 10, 2025
eaef5a1
oops, remove test
nedsalk Mar 10, 2025
e523109
oops
nedsalk Mar 10, 2025
7e279ba
fix lint
nedsalk Mar 10, 2025
845afa9
revert
nedsalk Mar 10, 2025
ec54c8c
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 10, 2025
a282163
a happy little accident
nedsalk Mar 10, 2025
4e1aed2
fix
nedsalk Mar 10, 2025
254d217
fix `deploy.test.ts`
nedsalk Mar 10, 2025
505d7d0
Merge branch 'ns/chore/fuel-core-0.42.0' into ns/feat/optimistic-conc…
nedsalk Mar 10, 2025
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
5 changes: 5 additions & 0 deletions .changeset/green-radios-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

chore: support RPC consistency mechanism
129 changes: 89 additions & 40 deletions packages/account/src/providers/fuel-graphql-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,30 @@ type FuelGraphQLSubscriberOptions = {
query: DocumentNode;
variables?: Record<string, unknown>;
fetchFn: typeof fetch;
operationName: string;
onEvent?: (event: FuelGraphqlSubscriberEvent) => void;
};

export interface FuelGraphqlSubscriberEvent {
data: unknown;
errors?: { message: string }[];
extensions?: Record<string, unknown>;
}

export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
public static incompatibleNodeVersionMessage: string | false = false;
private static textDecoder = new TextDecoder();

private constructor(private stream: ReadableStreamDefaultReader<Uint8Array>) {}

public static async create(options: FuelGraphQLSubscriberOptions) {
const { url, query, variables, fetchFn } = options;
const { url, query, variables, fetchFn, operationName, onEvent } = options;
const response = await fetchFn(`${url}-sub`, {
method: 'POST',
body: JSON.stringify({
query: print(query),
variables,
operationName,
}),
headers: {
'Content-Type': 'application/json',
Expand All @@ -35,7 +44,7 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
const [backgroundStream, resultStream] = response.body!.tee();

// eslint-disable-next-line no-void
void this.readInBackground(backgroundStream.getReader());
void this.readInBackground(backgroundStream.getReader(), onEvent);

const [errorReader, resultReader] = resultStream.tee().map((stream) => stream.getReader());

Expand All @@ -49,6 +58,60 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
return new FuelGraphqlSubscriber(resultReader);
}

public static async readEvent(
reader: ReadableStreamDefaultReader<Uint8Array>,
parsingLeftover: string = ''
): Promise<{
event: FuelGraphqlSubscriberEvent | undefined;
done: boolean;
parsingLeftover: string;
}> {
let text = parsingLeftover;
const regex = /data:.*\n\n/g;

// eslint-disable-next-line no-constant-condition
while (true) {
const matches = [...text.matchAll(regex)].flatMap((match) => match);

if (matches.length > 0) {
try {
const event = JSON.parse(matches[0].replace(/^data:/, ''));

return {
event,
done: false,
parsingLeftover: text.replace(matches[0], ''),
};
} catch (e) {
throw new FuelError(
ErrorCode.STREAM_PARSING_ERROR,
`Error while parsing stream data response: ${text}`
);
}
}

const { value, done } = await reader.read();

if (done) {
return { event: undefined, done, parsingLeftover: '' };
}

/**
* We don't care about keep-alive messages.
* The only responses that I came across from the node are either 200 responses with "data:.*" or keep-alive messages.
* You can find the keep-alive message in the fuel-core codebase (latest permalink as of writing):
* https://github.com/FuelLabs/fuel-core/blob/e1e631902f762081d2124d9c457ddfe13ac366dc/crates/fuel-core/src/graphql_api/service.rs#L247
* To get the actual latest info you need to check out the master branch (might change):
* https://github.com/FuelLabs/fuel-core/blob/master/crates/fuel-core/src/graphql_api/service.rs#L247
* */
const decoded = FuelGraphqlSubscriber.textDecoder
.decode(value)
.replace(':keep-alive-text\n\n', '');

text += decoded;
}
}

/**
* Reads the stream in the background,
* thereby preventing the stream from not being read
Expand All @@ -57,17 +120,29 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
* it is still available in the other streams
* via internal mechanisms related to teeing.
*/
private static async readInBackground(reader: ReadableStreamDefaultReader<Uint8Array>) {
private static async readInBackground(
reader: ReadableStreamDefaultReader<Uint8Array>,
onEvent?: (event: FuelGraphqlSubscriberEvent) => void
) {
let leftoverText = '';
// eslint-disable-next-line no-constant-condition
while (true) {
const { done } = await reader.read();
const { event, done, parsingLeftover } = await FuelGraphqlSubscriber.readEvent(
reader,
leftoverText
);

if (done) {
return;
}

onEvent?.(event as FuelGraphqlSubscriberEvent);
leftoverText = parsingLeftover;
}
}

private events: Array<{ data: unknown; errors?: { message: string }[] }> = [];
private events: Array<FuelGraphqlSubscriberEvent> = [];

private parsingLeftover = '';

async next(): Promise<IteratorResult<unknown, unknown>> {
Expand All @@ -79,44 +154,18 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
assertGqlResponseHasNoErrors(errors, FuelGraphqlSubscriber.incompatibleNodeVersionMessage);
return { value: data, done: false };
}
const { value, done } = await this.stream.read();
if (done) {
return { value, done };
}

/**
* We don't care about keep-alive messages.
* The only responses that I came across from the node are either 200 responses with "data:.*" or keep-alive messages.
* You can find the keep-alive message in the fuel-core codebase (latest permalink as of writing):
* https://github.com/FuelLabs/fuel-core/blob/e1e631902f762081d2124d9c457ddfe13ac366dc/crates/fuel-core/src/graphql_api/service.rs#L247
* To get the actual latest info you need to check out the master branch (might change):
* https://github.com/FuelLabs/fuel-core/blob/master/crates/fuel-core/src/graphql_api/service.rs#L247
* */
const decoded = FuelGraphqlSubscriber.textDecoder
.decode(value)
.replace(':keep-alive-text\n\n', '');

if (decoded === '') {
continue;
}
const { event, done, parsingLeftover } = await FuelGraphqlSubscriber.readEvent(
this.stream,
this.parsingLeftover
);

const text = `${this.parsingLeftover}${decoded}`;
const regex = /data:.*\n\n/g;

const matches = [...text.matchAll(regex)].flatMap((match) => match);

matches.forEach((match) => {
try {
this.events.push(JSON.parse(match.replace(/^data:/, '')));
} catch (e) {
throw new FuelError(
ErrorCode.STREAM_PARSING_ERROR,
`Error while parsing stream data response: ${text}`
);
}
});
this.parsingLeftover = parsingLeftover;

this.parsingLeftover = text.replace(matches.join(), '');
if (done) {
return { value: undefined, done: true };
}
this.events.push(event as FuelGraphqlSubscriberEvent);
}
}

Expand Down
60 changes: 60 additions & 0 deletions packages/account/src/providers/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,66 @@ describe('Provider', () => {
expect(spyOperation).toHaveBeenCalledTimes(1);
});

test('clearing cache based on URL only clears the cache for that URL', async () => {
using launched1 = await setupTestProviderAndWallets({
nodeOptions: {
args: ['--poa-instant', 'false', '--poa-interval-period', '10ms'],
loggingEnabled: false,
},
});
using launched2 = await setupTestProviderAndWallets({
nodeOptions: {
args: ['--poa-instant', 'false', '--poa-interval-period', '10ms'],
loggingEnabled: false,
},
});
const { provider: provider1 } = launched1;
const { provider: provider2 } = launched2;

// allow for block production
await sleep(200);

// update block height cache for both providers
await provider1.getLatestGasPrice();
await provider2.getLatestGasPrice();

Provider.clearChainAndNodeCaches(provider1.url);

// verify block height cache got reset correctly
const fetchSpy = vi.spyOn(global, 'fetch');

try {
await provider1.operations.submit({ encodedTransaction: '0x123' });
} catch (error) {
//
}
try {
await provider2.operations.submit({ encodedTransaction: '0x123' });
} catch (error) {
//
}

const {
extensions: { required_fuel_block_height: cache1BlockHeight },
} = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
const {
extensions: { required_fuel_block_height: cache2BlockHeight },
} = JSON.parse(fetchSpy.mock.calls[1][1]?.body as string);

expect(cache1BlockHeight).toEqual(0);
expect(cache2BlockHeight).toBeGreaterThan(0);

// verify nodeInfo and chainInfo caches got reset correctly
const getChainAndNodeInfoSpy1 = vi.spyOn(provider1.operations, 'getChainAndNodeInfo');
const getChainAndNodeInfoSpy2 = vi.spyOn(provider2.operations, 'getChainAndNodeInfo');

await provider1.fetchChainAndNodeInfo();
await provider2.fetchChainAndNodeInfo();

expect(getChainAndNodeInfoSpy1).toHaveBeenCalledTimes(1);
expect(getChainAndNodeInfoSpy2).toHaveBeenCalledTimes(0);
});

it('should ensure getGasConfig return essential gas related data', async () => {
using launched = await setupTestProviderAndWallets();
const { provider } = launched;
Expand Down
Loading
Loading