diff --git a/__tests__/detailed/bot_events.ts b/__tests__/detailed/bot_events.ts index 1975c0cb25f7167347cfbcd14d8c366ff282a008..3967e13873b334ee41e4becb63d0ca2a33c2ece3 100644 --- a/__tests__/detailed/bot_events.ts +++ b/__tests__/detailed/bot_events.ts @@ -121,7 +121,7 @@ test.describe("WorkerBee Bot events test", () => { let blocksParsed = 0; const observer = bot.observe.onBlock().subscribe({ next(data) { - console.info(`Got block #${data.block.number}`); + console.info(`Got block #${data.block!.number}`); ++blocksParsed; }, error(err) { @@ -141,6 +141,36 @@ test.describe("WorkerBee Bot events test", () => { expect(result).toBeGreaterThanOrEqual(1); }); + test("Should not fail when async next callback fails", async({ workerbeeTest }) => { + const result = await workerbeeTest.dynamic(async({ WorkerBee }, hiveBlockInterval) => { + const bot = new WorkerBee(); + + let blocksParsed = 0; + const observer = bot.observe.onBlock().subscribe({ + /* eslint-disable-next-line require-await */ + async next(data) { + console.info(`Got block #${data.block!.number}`); + ++blocksParsed; + + throw new Error("Intentional error in next()"); + }, + error(err) { + console.error(err); + } + }); + + await bot.start(); + + await new Promise(res => { setTimeout(res, hiveBlockInterval * 2); }); + + observer.unsubscribe(); + + return blocksParsed; + }, HIVE_BLOCK_INTERVAL); + + expect(result).toBeGreaterThanOrEqual(1); + }); + test("Should be able to analyze blocks successively", async({ workerbeeTest }) => { test.setTimeout(60_000); await workerbeeTest.dynamic(async({ WorkerBee }, hiveBlockInterval) => { @@ -152,16 +182,16 @@ test.describe("WorkerBee Bot events test", () => { await new Promise(async (resolve, reject) => { const observer = bot.observe.onBlock().provideBlockData().subscribe({ next(data) { - console.info(`Got block #${data.block.number}`); + console.info(`Got block #${data.block!.number}`); if (typeof blockNumber !== "undefined") - if (blockNumber + 1 !== data.block.number) { - console.error(`Blocks are not consecutive: ${blockNumber} followed by ${data.block.number}`); + if (blockNumber + 1 !== data.block!.number) { + console.error(`Blocks are not consecutive: ${blockNumber} followed by ${data.block!.number}`); reject(new Error("Blocks are not consecutive")); return; } - blockNumber = data.block.number; + blockNumber = data.block!.number; }, error(err) { console.error(err); @@ -295,8 +325,8 @@ test.describe("WorkerBee Bot events test", () => { let rshares: number | undefined; for(const account in data.commentsMetadata) - for(const permlink in data.commentsMetadata[account]) { - rshares = Number(data.commentsMetadata[account][permlink].netRshares); + for(const permlink in data.commentsMetadata[account as "gtg"]) { + rshares = Number(data.commentsMetadata[account as "gtg"]![permlink].netRshares); console.info(`Retrieved comment payout of @${account}: ${rshares} rshares for ${permlink}`); } @@ -418,7 +448,7 @@ test.describe("WorkerBee Bot events test", () => { await new Promise(resolve => { bot.providePastOperations(500017, 500020).onBlock().provideBlockHeaderData().subscribe({ next(data) { - console.log(`Got block #${data.block.number}`); + console.log(`Got block #${data.block!.number}`); ++calls; }, @@ -455,7 +485,7 @@ test.describe("WorkerBee Bot events test", () => { next(data) { gotTx = true; - console.log(`Got transaction #${block!.transaction_ids[0]} in block ${data.block.number}: ${ + console.log(`Got transaction #${block!.transaction_ids[0]} in block ${data.block!.number}: ${ data.transactions[block!.transaction_ids[0]]!.operations.length} operations`); }, error(err) { @@ -516,7 +546,7 @@ test.describe("WorkerBee Bot events test", () => { return; data.impactedAccounts["lolzbot"].forEach(({ transaction }) => { - console.log(`Got transaction #${transaction.id} for lolzbot in block #${data.block.number}`); + console.log(`Got transaction #${transaction.id} for lolzbot in block #${data.block!.number}`); ++calls; }); @@ -549,7 +579,7 @@ test.describe("WorkerBee Bot events test", () => { observer.onBlock().provideBlockData().subscribe({ next(data) { - console.log(`Got block #${data.block.number}`); + console.log(`Got block #${data.block!.number}`); ++calls; }, diff --git a/__tests__/detailed/complex_scenarios_test_plan.md b/__tests__/detailed/complex_scenarios_test_plan.md deleted file mode 100644 index ae90cef4e1edfb79054ca5a8a11bf5d73ae3789e..0000000000000000000000000000000000000000 --- a/__tests__/detailed/complex_scenarios_test_plan.md +++ /dev/null @@ -1,546 +0,0 @@ -# Test Plan - Complex WorkerBee Usage Scenarios - -## Individual Filter Verification Scenarios - -### 1. onPosts Filter Tests - -#### 1.1 onPosts Positive Cases -```typescript -// Multiple accounts - should trigger when any specified account creates post -bot.observe.onPosts("author1", "author2", "author3") - -// Simultaneous posts - should trigger for all accounts posting in same block -bot.observe.onPosts("author1", "author2") // Both post in block N -``` - -#### 1.2 onPosts Negative Cases -```typescript -// Should NOT trigger when account creates comment -bot.observe.onPosts("test-author") // test-author creates comment, not post - -// Multiple accounts - should NOT trigger when any specified account creates comment, not post -bot.observe.onPosts("author1", "author2", "author3") - -// Monitor for posts from a specific account - should NOT trigger as the account created no posts -bot.observe.onPosts("nonexistent-account") - -// Should handle empty account list -bot.observe.onPosts() -``` - -### 2. onComments Filter Tests - -#### 2.1 onComments Positive Cases -```typescript -// Multiple accounts - should trigger when any specified account creates comment -bot.observe.onComments("commenter1", "commenter2", "commenter3") - -// Simultaneous comments - should trigger for all accounts commenting in same block -bot.observe.onComments("commenter1", "commenter2") // Both comment in block N -``` - -#### 2.2 onComments Negative Cases -```typescript -// Should NOT trigger when account creates post -bot.observe.onComments("test-commenter") // test-commenter creates post, not comment - -// Multiple accounts - should NOT trigger when any specified account creates post, not comment -bot.observe.onComments("author1", "author2", "author3") - -// Monitor for comments from a specific account - should NOT trigger as the account created no comments -bot.observe.onComments("nonexistent-account") - -// Should handle empty account list -bot.observe.onComments() -``` - -#### 2.3 onPosts and onComments cases -```typescript -// Multiple accounts - should trigger when any specified account creates post or comment -bot.observe.onComments("test-commenter").or.onPosts("test-poster") -``` - -### 3. onVotes Filter Tests - -#### 3.1 onVotes Positive Cases -```typescript -// Multiple voters - should trigger when any specified account votes -bot.observe.onVotes("voter1", "voter2", "voter3") - -// Simultaneous votes - should trigger for all accounts voting in same block -bot.observe.onVotes("voter1", "voter2") // Both vote in block N -``` - -#### 3.2 onVotes Negative Cases -```typescript -// Should NOT trigger when account creates post/comment -bot.observe.onVotes("test-voter") // test-voter posts or comments, doesn't vote - -// Should NOT trigger when different account votes -bot.observe.onVotes("voter1") // voter2 votes, not voter1 - -// Monitor for votes from a specific account - should NOT trigger as the account did not vote -bot.observe.onVotes("nonexistent-account") - -// Should handle empty account list -bot.observe.onVotes() -``` - -## Realistic Test Scenarios - -### 1. Scenarios with OR operator - -#### 1.1 Multi-Author Content Monitor -```typescript -// Monitors posts OR comments from multiple authors -bot.observe.onPosts("author1").or.onPosts("author2").or.onComments("author1") -``` - -#### 1.2 Social Activity Aggregator -```typescript -// Tracks various types of social activity -bot.observe.onVotes("curator").or.onFollow("influencer").or.onReblog("curator") -``` - -#### 1.3 Financial Activity Monitor -```typescript -// Monitors financial activity -bot.observe.onWhaleAlert(hiveCoins(1000)).or.onInternalMarketOperation().or.onExchangeTransfer() -``` - -#### 1.4 Content Engagement Tracker -```typescript -// Tracks content engagement -bot.observe.onMention("brand").or.onPosts("brand").or.onReblog("brand") -``` - -#### 1.5 Cross-Platform Activity Monitor -```typescript -// Monitors activity across different platforms and operations -bot.observe.onCustomOperation("follow") - .or.onCustomOperation("reblog") - .or.onNewAccount() - .or.onAlarm("monitored-account") -``` - -#### 1.6 Content Creator Dashboard -```typescript -// Comprehensive content creator monitoring -bot.observe.onPosts("creator") - .or.onComments("creator") - .or.onMention("creator") - .or.onReblog("creator") - .or.onVotes("creator") -``` - -#### 1.7 Market Movement Detector -```typescript -// Detects significant market movements -bot.observe.onFeedPriceChange(3) - .or.onWhaleAlert(hiveCoins(10000)) - .or.onInternalMarketOperation() - .or.onExchangeTransfer() -``` - -#### 1.8 Witness Reliability Monitor -```typescript -// Monitors witness performance and reliability -bot.observe.onWitnessesMissedBlocks(5, "witness1") - .or.onWitnessesMissedBlocks(5, "witness2") - .or.onFeedPriceNoChange(48) -``` - -### 2. Historical Data Scenarios - -#### 2.1 Pattern Analysis Bot -```typescript -// Analyzes voting patterns in the past -bot.providePastOperations(startBlock, endBlock) - .onVotes("whale") - .or.onPosts("whale") - .provideManabarData(EManabarType.RC, "whale") -``` - -#### 2.2 Market Trend Analyzer -```typescript -// Analyzes market trends from the past -bot.providePastOperations("-30d") - .onWhaleAlert(hiveCoins(5000)) - .or.onInternalMarketOperation() - .provideBlockData() -``` - -#### 2.3 Community Growth Monitor -```typescript -// Monitors community growth -bot.providePastOperations(startBlock, endBlock) - .onNewAccount() - .or.onFollow("community-account") - .or.onCustomOperation("follow") -``` - -#### 2.4 Content Performance Analyzer -```typescript -// Analyzes content performance over time -bot.providePastOperations("-14d") - .onPosts("top-author") - .or.onComments("top-author") - .or.onVotes("top-author") - .provideAccounts("top-author") - .provideManabarData(EManabarType.UPVOTE, "top-author") -``` - -#### 2.5 Economic Activity Tracker -```typescript -// Tracks economic activity patterns -bot.providePastOperations(recentBlocks) - .onWhaleAlert(hiveCoins(1000)) - .or.onExchangeTransfer() - .or.onInternalMarketOperation() - .provideBlockData() - .provideFeedPriceData() -``` - -#### 2.6 Account Behavior Analysis -```typescript -// Analyzes account behavior patterns -bot.providePastOperations("-7d") - .onPosts("analyzed-account") - .or.onVotes("analyzed-account") - .or.onFollow("analyzed-account") - .or.onReblog("analyzed-account") - .provideAccounts("analyzed-account") -``` - -### 3. Live Monitoring Scenarios - -#### 3.1 Real-time Social Dashboard -```typescript -// Real-time activity dashboard -bot.observe.onPosts("featured-author") - .or.onComments("featured-author") - .or.onVotes("featured-author") - .provideAccounts("featured-author") - .provideManabarData(EManabarType.RC, "featured-author") -``` - -#### 3.2 Account Health Monitor -```typescript -// Monitors account "health" -bot.observe.onAccountsBalanceChange(false, "monitored-account") - .or.onAccountsMetadataChange("monitored-account") - .or.onAccountsManabarPercent(EManabarType.RC, 20, "monitored-account") -``` - -#### 3.3 Market Alert System -```typescript -// Market alert system -bot.observe.onFeedPriceChange(5) - .or.onFeedPriceNoChange(24) - .or.onWitnessesMissedBlocks(10, "witness") - .provideFeedPriceData() -``` - -#### 3.4 Community Moderation Bot (TODO after `onCommunityPost` implementation) -```typescript -// Community moderation monitoring -bot.observe.onPosts("community-tag") - .or.onComments("moderator-account") - .or.onCustomOperation("community") - .or.onMention("community-account") -``` - -#### 3.5 Investment Portfolio Monitor -```typescript -// Monitors investment portfolio activities -bot.observe.onAccountsBalanceChange(true, "investor1", "investor2") - .or.onWhaleAlert(hiveCoins(5000)) - .or.onExchangeTransfer() - .provideAccounts("investor1", "investor2") -``` - -#### 3.6 Content Aggregation Service -```typescript -// Aggregates content from multiple sources -bot.observe.onPosts("news-account1") - .or.onPosts("news-account2") - .or.onReblog("aggregator-account") -``` - -#### 3.7 Engagement Optimization Bot -```typescript -// Optimizes engagement timing -bot.observe.onAccountsManabarPercent(EManabarType.UPVOTE, 90, "curator") - .or.onPosts("target-author") - .or.onComments("target-author") - .provideManabarData(EManabarType.UPVOTE, "curator") -``` - -### 4. Complex Scenarios with Providers - -#### 4.1 Complete Account Analysis -```typescript -// Complete account analysis with all data -bot.observe.onBlock() - .provideAccounts("target-account") - .provideRcAccounts("target-account") - .provideManabarData(EManabarType.RC, "target-account") - .provideManabarData(EManabarType.UPVOTE, "target-account") - .provideBlockData() - .provideFeedPriceData() -``` - -#### 4.2 Multi-Account Comparison -```typescript -// Multi-account comparison -bot.observe.onBlock() - .provideAccounts("account1", "account2", "account3") - .provideManabarData(EManabarType.RC, "account1", "account2", "account3") - .provideWitnesses("witness1", "witness2") -``` - -#### 4.3 Comprehensive Market Analysis -```typescript -// Full market data analysis -bot.observe.onBlock() - .or.onInternalMarketOperation() - .or.onExchangeTransfer() - .provideBlockData() - .provideFeedPriceData() - .provideAccounts("major-trader1", "major-trader2") -``` - -#### 4.4 Social Network Analysis -```typescript -// Social network relationship analysis -bot.observe.onFollow("influencer1") - .or.onFollow("influencer2") - .or.onReblog("influencer1") - .or.onMention("influencer1") - .provideAccounts("influencer1", "influencer2") - .provideManabarData(EManabarType.RC, "influencer1", "influencer2") -``` - -#### 4.5 Content Performance Dashboard -```typescript -// Comprehensive content performance monitoring -bot.observe.onPosts("content-creator") - .or.onComments("content-creator") - .or.onVotes("content-creator") - .or.onReblog("content-creator") - .provideAccounts("content-creator") - .provideManabarData(EManabarType.UPVOTE, "content-creator") - .provideBlockData() -``` - -#### 4.6 Governance Monitoring System -```typescript -// Monitors governance-related activities -bot.observe.onWitnessesMissedBlocks(3, "witness1", "witness2") - .or.onFeedPriceChange(2) - .or.onCustomOperation("witness_set_properties") - .provideWitnesses("witness1", "witness2") - .provideFeedPriceData() -``` - -### 5. Performance and Scalability Testing - -#### 5.1 High-Volume Transaction Monitor -```typescript -// Monitor large number of transactions -bot.providePastOperations(largeDaysRange) - .onTransactionIds(...manyTransactionIds) - .provideBlockData() -``` - -#### 5.2 Multi-Filter Performance Test -```typescript -// Performance test with multiple filters -bot.observe.onPosts("author1") - .or.onPosts("author2") - .or.onComments("author1") - .or.onComments("author2") - .or.onVotes("author1") - .or.onVotes("author2") -``` - -#### 5.3 Massive Account Monitoring -```typescript -// Monitoring large number of accounts -bot.observe.onAccountsBalanceChange(false, ...hundredsOfAccounts) - .or.onAccountsMetadataChange(...hundredsOfAccounts) - .provideAccounts(...hundredsOfAccounts) -``` - -#### 5.4 High-Frequency Event Processing -```typescript -// Processing high-frequency events -bot.observe.onBlock() - .or.onVotes("high-activity-curator") - .or.onComments("popular-author") - .or.onInternalMarketOperation() - .provideBlockData() -``` - -### 6. Advanced Use Cases - -#### 6.1 Economic Research Platform -```typescript -// Economic research data collection -bot.observe.onWhaleAlert(hiveCoins(10000)) - .or.onFeedPriceChange(1) - .or.onInternalMarketOperation() - .or.onExchangeTransfer() - .provideFeedPriceData() - .provideBlockData() -``` - -#### 6.2 Content Recommendation Engine -```typescript -// Content recommendation data gathering -bot.observe.onPosts("popular-tag") - .or.onReblog("aggregator-accounts") - .or.onVotes("quality-curators") - .or.onMention("trending-topics") - .provideAccounts("popular-authors") -``` - -#### 6.3 Automated Trading Signal Generator -```typescript -// Generating trading signals from blockchain activity -bot.observe.onWhaleAlert(hiveCoins(50000)) - .or.onFeedPriceChange(3) - .or.onWitnessesMissedBlocks(5, "top-witnesses") - .or.onInternalMarketOperation() - .provideFeedPriceData() - .provideWitnesses("top-witnesses") -``` - -### 7. Edge Cases and Stress Testing - -#### 7.1 Multiple OR Chaining Test -```typescript -// Testing extreme OR chaining -bot.observe.onPosts("author1") - .or.onPosts("author2") - .or.onPosts("author3") - .or.onPosts("author4") - .or.onComments("author1") - .or.onComments("author2") - .or.onVotes("curator1") - .or.onVotes("curator2") - .or.onFollow("influencer") - .or.onReblog("aggregator") -``` - -#### 7.2 Repeated OR Operations Test -```typescript -// Testing repeated .or.or.or.or operations -bot.observe.onBlock() - .or.or.or.or.onPosts("test-author") - .provideBlockData() -``` - -#### 7.3 Duplicate Provider Calls Test -```typescript -// Testing duplicate provider calls with same arguments -bot.observe.onBlock() - .provideAccounts("gtg") - .provideAccounts("gtg") - .provideAccounts("gtg") - .provideAccounts("gtg") - .provideManabarData(EManabarType.RC, "gtg") - .provideManabarData(EManabarType.RC, "gtg") - .provideManabarData(EManabarType.RC, "gtg") - .provideBlockData() - .provideBlockData() - .provideBlockData() -``` - -#### 7.4 Provider Type Duplication Test -```typescript -// Testing same provider types with different arguments -bot.observe.onBlock() - .provideAccounts("account1") - .provideAccounts("account2") - .provideAccounts("account3") - .provideManabarData(EManabarType.RC, "account1") - .provideManabarData(EManabarType.UPVOTE, "account1") - .provideManabarData(EManabarType.DOWNVOTE, "account1") - .provideManabarData(EManabarType.RC, "account2") -``` - -#### 7.5 Complex Nested Filter Combinations -```typescript -// Complex combination of multiple filter types -bot.observe.onPosts("author1") - .or.onComments("author1") - .or.onVotes("author1") - .or.onFollow("author1") - .or.onReblog("author1") - .or.onMention("author1") - .or.onWhaleAlert(hiveCoins(1000)) - .or.onInternalMarketOperation() - .or.onExchangeTransfer() - .or.onNewAccount() - .or.onCustomOperation("follow") - .or.onAccountsBalanceChange(false, "author1") - .or.onAccountsMetadataChange("author1") - .or.onFeedPriceChange(2) - .provideAccounts("author1") - .provideManabarData(EManabarType.RC, "author1") - .provideBlockData() - .provideFeedPriceData() -``` - -#### 7.6 Double Subscribe Prevention Test (TODO after resolving issue #20) -```typescript -// Testing prevention of double subscribe -const observer = bot.observe.onPosts("test-author"); - -// First subscribe -const subscription1 = observer.subscribe({ - next(data) { console.log("First subscription"); } -}); - -// Attempt second subscribe (should be prevented or handled gracefully) -try { - const subscription2 = observer.subscribe({ - next(data) { console.log("Second subscription - should not work"); } - }); - // This should either throw an error or be handled gracefully -} catch (error) { - console.log("Double subscribe prevented:", error.message); -} -``` - -#### 7.7 Resource Cleanup Stress Test -```typescript -// Testing proper cleanup under stress -const observers = []; -for (let i = 0; i < 100; i++) { - const observer = bot.observe.onPosts(`author${i}`) - .or.onComments(`author${i}`) - .provideAccounts(`author${i}`); - - observers.push(observer); -} - -// Cleanup all observers -observers.forEach(observer => observer.unsubscribe()); -``` - -#### 7.8 Concurrent Observer Test -```typescript -// Testing multiple concurrent observers -const observer1 = bot.observe.onPosts("author1").provideAccounts("author1"); -const observer2 = bot.observe.onComments("author2").provideAccounts("author2"); -const observer3 = bot.observe.onVotes("curator").provideManabarData(EManabarType.RC, "curator"); - -// All should work simultaneously without interference -Promise.all([ - new Promise(resolve => observer1.subscribe({ next: resolve })), - new Promise(resolve => observer2.subscribe({ next: resolve })), - new Promise(resolve => observer3.subscribe({ next: resolve })) -]); -``` diff --git a/__tests__/detailed/individual_filter_tests.ts b/__tests__/detailed/individual_filter_tests.ts index 948d6917b76955059770f0834a550abc64e10601..e1177cf5f7374412e44b60990ec29f88914fab7e 100644 --- a/__tests__/detailed/individual_filter_tests.ts +++ b/__tests__/detailed/individual_filter_tests.ts @@ -10,7 +10,7 @@ test.describe("WorkerBee Individual Filter Verification", () => { bot.onPosts("mtyszczak", "author2", "author3").subscribe({ next(data) { for (const author of ["mtyszczak", "author2", "author3"]) - data.posts[author]?.forEach(({ operation }) => { + data.posts[author as "mtyszczak" | "author2" | "author3"]?.forEach(({ operation }) => { capturedPosts.push(`Post by ${operation.author}: ${operation.permlink}`); }); }, @@ -32,10 +32,10 @@ test.describe("WorkerBee Individual Filter Verification", () => { bot.onBlock().onPosts("comandoyeya", "daddydog").subscribe({ next(data) { for (const author of ["comandoyeya", "daddydog"]) - data.posts[author]?.forEach(({ operation }) => { + data.posts[author as "comandoyeya" | "daddydog"]?.forEach(({ operation }) => { capturedPosts.push({ author: operation.author, - blockNumber: data.block.number + blockNumber: data.block!.number }); }); @@ -197,7 +197,7 @@ test.describe("WorkerBee Individual Filter Verification", () => { (bot).onComments("gtg", "moretea", "khantaimur").subscribe({ next(data) { for (const author of ["gtg", "moretea", "khantaimur"]) - data.comments[author]?.forEach(({ operation }) => { + data.comments[author as "gtg" | "moretea" | "khantaimur"]?.forEach(({ operation }) => { capturedComments.push(`Comment by ${operation.author}: ${operation.permlink}`); }); }, @@ -223,10 +223,10 @@ test.describe("WorkerBee Individual Filter Verification", () => { (bot).onBlock().onComments("zayyar99", "beckyroyal").subscribe({ next(data) { for (const author of ["zayyar99", "beckyroyal"]) - data.comments[author]?.forEach(({ operation }) => { + data.comments[author as "zayyar99" | "beckyroyal"]?.forEach(({ operation }) => { capturedComments.push({ author: operation.author, - blockNumber: data.block.number + blockNumber: data.block!.number }); }); }, @@ -391,11 +391,11 @@ test.describe("WorkerBee Individual Filter Verification", () => { .subscribe({ next(data) { for (const author of ["mtyszczak", "secret-art", "author2", "author3"]) { - data.comments[author]?.forEach(({ operation }) => { + data.comments[author as "secret-art" | "author2" | "author3"]?.forEach(({ operation }) => { capturedPosts.push(`Comment by ${operation.author}: ${operation.permlink}`); }); - data.posts[author]?.forEach(({ operation }) => { + data.posts[author as "mtyszczak" | "author2" | "author3"]?.forEach(({ operation }) => { capturedPosts.push(`Post by ${operation.author}: ${operation.permlink}`); }); } @@ -424,7 +424,7 @@ test.describe("WorkerBee Individual Filter Verification", () => { (bot).onVotes("dhedge", "winanda").subscribe({ next(data) { for (const voter of ["dhedge", "winanda"]) - data.votes[voter]?.forEach(({ operation }) => { + data.votes[voter as "dhedge" | "winanda"]?.forEach(({ operation }) => { capturedVotes.push(`Vote by ${operation.voter} on ${operation.author}/${operation.permlink}`); }); }, @@ -450,10 +450,10 @@ test.describe("WorkerBee Individual Filter Verification", () => { (bot).onBlock().onVotes("noctury", "the-burn").subscribe({ next(data) { for (const voter of ["noctury", "the-burn"]) - data.votes[voter]?.forEach(({ operation }) => { + data.votes[voter as "noctury" | "the-burn"]?.forEach(({ operation }) => { capturedVotes.push({ voter: operation.voter, - blockNumber: data.block.number + blockNumber: data.block!.number }); }); }, @@ -582,11 +582,11 @@ test.describe("WorkerBee Individual Filter Verification", () => { next(data) { for (const author of ["mtyszczak", "jacor"]) { data.posts[author as keyof typeof data["posts"]]?.forEach(({ operation }) => { - capturedPosts.push(`Post by ${operation.author}: ${operation.permlink} in block ${data.block.number}`); + capturedPosts.push(`Post by ${operation.author}: ${operation.permlink} in block ${data.block!.number}`); }); data.votes[author as keyof typeof data["votes"]]?.forEach(({ operation }) => { - capturedPosts.push(`Vote by ${operation.voter}: ${operation.permlink} in block ${data.block.number}`); + capturedPosts.push(`Vote by ${operation.voter}: ${operation.permlink} in block ${data.block!.number}`); }); } diff --git a/__tests__/detailed/mock_realistic_scenarios_live_data.ts b/__tests__/detailed/mock_realistic_scenarios_live_data.ts index 2960c75c4c3637aa36f085f5b00c2c4e46d6b2b2..0d55db81a6df8dc5ff95abe2ad8bd80540148802 100644 --- a/__tests__/detailed/mock_realistic_scenarios_live_data.ts +++ b/__tests__/detailed/mock_realistic_scenarios_live_data.ts @@ -130,7 +130,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { next(data) { let percentChange = 0; - const priceHistoryArray = Array.from(data.feedPrice.priceHistory); + const priceHistoryArray = Array.from(data.feedPrice!.priceHistory); const price1 = Number.parseInt(priceHistoryArray[0].base!.amount) / Number.parseInt(priceHistoryArray[0].quote!.amount); const price2 = Number.parseInt(priceHistoryArray[1].base!.amount) / Number.parseInt(priceHistoryArray[1].quote!.amount); @@ -270,8 +270,10 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { content.push(`Account: ${data.accounts.gtg?.name}, Balance: ${data.accounts.gtg?.balance.HBD.savings.amount}`); content.push(`RC mana: ${data.rcAccounts.gtg?.rcManabar.currentMana}`); content.push(`UPVOTE mana: ${data.manabarData.gtg?.[0]?.currentMana}`); - content.push(`Block number: ${data.block.number}`); - content.push(`Feed price: ${Array.from(data.feedPrice.priceHistory)[0].base!.amount}/${Array.from(data.feedPrice.priceHistory)[0].quote!.amount}`); + content.push(`Block number: ${data.block!.number}`); + content.push( + `Feed price: ${Array.from(data.feedPrice!.priceHistory)[0].base!.amount}/${Array.from(data.feedPrice!.priceHistory)[0].quote!.amount}` + ); if (content.length >= 5) resolve(content); @@ -348,11 +350,11 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { .subscribe({ next(data) { data.internalMarketOperations.forEach(({ operation }) => { - content.push(`Internal market operation: owner: ${operation.owner}, order id: ${operation.orderId}, block : ${data.block.number}`); + content.push(`Internal market operation: owner: ${operation.owner}, order id: ${operation.orderId}, block : ${data.block!.number}`); }); data.exchangeTransferOperations.forEach(({ operation }) => { - content.push(`Exchange transfer: ${operation.from} -> ${operation.to} (${operation.amount.amount}), block: ${data.block.number}`); + content.push(`Exchange transfer: ${operation.from} -> ${operation.to} (${operation.amount.amount}), block: ${data.block!.number}`); }); if (content.length >= 2) @@ -486,7 +488,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { if (data.witnesses.blocktrades && data.witnesses.blocktrades.totalMissedBlocks >= 3) content.push(`Witness missed blocks: blocktrades - ${data.witnesses.blocktrades.totalMissedBlocks}`); - const priceHistoryArray = Array.from(data.feedPrice.priceHistory); + const priceHistoryArray = Array.from(data.feedPrice!.priceHistory); if (priceHistoryArray.length >= 2) { const price1 = Number.parseInt(priceHistoryArray[0].base!.amount) / Number.parseInt(priceHistoryArray[0].quote!.amount); @@ -700,7 +702,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { .provideBlockData() .subscribe({ next(data) { - content.push(`Block processed: ${data.block.number}`); + content.push(`Block processed: ${data.block!.number}`); data.votes.gtg?.forEach(({ operation }) => { content.push(`High-frequency vote: ${operation.voter} -> ${operation.author}`); @@ -748,7 +750,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { content.push(`Economic whale alert: ${operation.from} -> ${operation.to} (${operation.amount.amount})`); }); - const priceHistoryArray = Array.from(data.feedPrice.priceHistory); + const priceHistoryArray = Array.from(data.feedPrice!.priceHistory); if (priceHistoryArray.length >= 2) { const price1 = Number.parseInt(priceHistoryArray[0].base!.amount) / Number.parseInt(priceHistoryArray[0].quote!.amount); const price2 = Number.parseInt(priceHistoryArray[1].base!.amount) / Number.parseInt(priceHistoryArray[1].quote!.amount); @@ -851,7 +853,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { content.push(`Trading signal - Large whale movement: ${operation.from} -> ${operation.to} (${operation.amount.amount})`); }); - const priceHistoryArray = Array.from(data.feedPrice.priceHistory); + const priceHistoryArray = Array.from(data.feedPrice!.priceHistory); if (priceHistoryArray.length >= 2) { const price1 = Number.parseInt(priceHistoryArray[0].base!.amount) / Number.parseInt(priceHistoryArray[0].quote!.amount); const price2 = Number.parseInt(priceHistoryArray[1].base!.amount) / Number.parseInt(priceHistoryArray[1].quote!.amount); @@ -969,7 +971,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { .provideBlockData() .subscribe({ next(data) { - content.push(`Repeated OR block: ${data.block.number}`); + content.push(`Repeated OR block: ${data.block!.number}`); data.posts.gtg?.forEach(({ operation }) => { content.push(`Repeated OR post: ${operation.author} - ${operation.title}`); @@ -1008,7 +1010,7 @@ mockTest.describe("Realistic Scenarios with Live Data", () => { .provideBlockData() .subscribe({ next(data) { - content.push(`Duplicate providers block: ${data.block.number}`); + content.push(`Duplicate providers block: ${data.block!.number}`); content.push(`Duplicate providers account: ${data.accounts.gtg?.name}`); content.push(`Duplicate providers manabar: ${data.manabarData.gtg?.[2]?.currentMana}`); diff --git a/__tests__/detailed/realistic_scenarios.ts b/__tests__/detailed/realistic_scenarios.ts index cc23026ab488ddf1eb8b5965312ec097d4ed0590..980175a8ac6dcdd47b6977799062d845e32c1a80 100644 --- a/__tests__/detailed/realistic_scenarios.ts +++ b/__tests__/detailed/realistic_scenarios.ts @@ -11,10 +11,10 @@ test.describe("Bot Realistic Scenarios", () => { bot.onPosts("mtyszczak").onPosts("nickdongsik").onComments("brando28").subscribe({ next(data) { for (const author in data.posts) - data.posts[author].forEach(({ operation }) => content.push(`${operation.author} - ${operation.permlink}`)); + data.posts[author as keyof typeof data.posts]?.forEach(({ operation }) => content.push(`${operation.author} - ${operation.permlink}`)); for (const author in data.comments) - data.comments[author].forEach(({ operation }) => content.push(`${operation.author} - ${operation.permlink}`)); + data.comments[author as keyof typeof data.comments]?.forEach(({ operation }) => content.push(`${operation.author} - ${operation.permlink}`)); }, error: (err) => { console.error(err); @@ -38,13 +38,19 @@ test.describe("Bot Realistic Scenarios", () => { bot.onVotes("e-sport-gamer").onFollow("fwaszkiewicz").onReblog("maxinpower").subscribe({ next(data) { for (const author in data.votes) - data.votes[author].forEach(({ operation }) => content.push(`Vote: ${operation.voter} - ${operation.permlink}`)); + data.votes[author as keyof typeof data.votes]?.forEach(({ operation }) => { + content.push(`Vote: ${operation.voter} - ${operation.permlink}`) + }); for (const author in data.follows) - data.follows[author].forEach(({ operation }) => content.push(`Follow: ${operation.follower} - ${operation.following}`)); + data.follows[author as keyof typeof data.follows]?.forEach(({ operation }) => { + content.push(`Follow: ${operation.follower} - ${operation.following}`) + }); for (const author in data.reblogs) - data.reblogs[author].forEach(({ operation }) => content.push(`Reblog: ${operation.author} - ${operation.permlink}`)); + data.reblogs[author as keyof typeof data.reblogs]?.forEach(({ operation }) => { + content.push(`Reblog: ${operation.author} - ${operation.permlink}`) + }); }, error: (err) => { console.error(err); diff --git a/package.json b/package.json index 4110f403395c7d8c3b1a68d32b350ebbd96ff330..a90b7a4d7643b60ccb3872289ecc41aa0e024440 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "path": [ "./dist/bundle" ], - "limit": "175 kB", + "limit": "180 kB", "brotli": false } ], diff --git a/src/bot.ts b/src/bot.ts index f55a0f63d9ae32ccb70b113f3fb8a5af699017dc..7d9a8d292db6939e170877e39cf5e2b6f2c62363 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -115,6 +115,9 @@ export class WorkerBee implements IWorkerBee | unde next(val) { const transaction = val.transactions[apiTx.id] || val.transactions[apiTx.legacy_id]; if (transaction === undefined) { + if (val.block === undefined) + throw new WorkerBeeError("Block data is missing in the block event"); + blocksAnalyzed.push(val.block.number); return; @@ -205,6 +208,9 @@ export class WorkerBee implements IWorkerBee | unde // Create a single observer that will listen for block data const observer = this.observe.onBlock().provideBlockData().subscribe({ next: data => { + if (data.block === undefined) + return; // Ignore empty block data + if(promiseToResolveCb !== undefined) { // Resolve the waiting promise in queue with the block data and pass it to the next iteration of user loop promiseToResolveCb({ value: data.block, done: false }); diff --git a/src/chain-observers/filters/blank-filter.ts b/src/chain-observers/filters/blank-filter.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba73b167b669e8afb97fbf135b46ad13b5da5ca5 --- /dev/null +++ b/src/chain-observers/filters/blank-filter.ts @@ -0,0 +1,20 @@ +import type { WorkerBee } from "../../bot"; +import type { TRegisterEvaluationContext } from "../classifiers/collector-classifier-base"; +import { FilterBase } from "./filter-base"; + +export class BlankFilter extends FilterBase { + public constructor( + worker: WorkerBee + ) { + super(worker); + } + + public usedContexts(): Array { + return []; + } + + /* eslint-disable-next-line require-await */ + public async match(): Promise { + return true; + } +} diff --git a/src/chain-observers/observer-mediator.ts b/src/chain-observers/observer-mediator.ts index 64c40d79cd6bcf519380679c28717a799f563a5e..286b08074167f30a55a219630fb2c053b26caba4 100644 --- a/src/chain-observers/observer-mediator.ts +++ b/src/chain-observers/observer-mediator.ts @@ -50,8 +50,9 @@ export class ObserverMediator { // Launch all providers in parallel const allDataToProvide: Promise[] = []; + // Call a provider, catch errors and forward data (either full on success or base structure on error for type consistency) for(const provider of providers) - allDataToProvide.push(provider.provide(context).catch(error => listener.error?.(error))); + allDataToProvide.push(provider.provide(context).catch(error => { listener.error?.(error); return provider.baseStructure; })); // Wait for all providers to finish and merge their results for(const promiseData of allDataToProvide) { @@ -63,7 +64,7 @@ export class ObserverMediator { this.factory.addTiming("providers", Date.now() - startProvider); - listener.next?.(providedData); + await (listener.next?.(providedData) as Promise | any); }).catch(error => listener.error?.(error)); this.factory.postNotify(this, context); diff --git a/src/chain-observers/providers/account-provider.ts b/src/chain-observers/providers/account-provider.ts index 3356cced445376539da24cdce12c860abf6aef2a..3331c4e743aae191450e739a9daadd9eeab7747c 100644 --- a/src/chain-observers/providers/account-provider.ts +++ b/src/chain-observers/providers/account-provider.ts @@ -17,7 +17,9 @@ export interface IAccountProviderOptions { accounts: string[]; } -export class AccountProvider = Array> extends ProviderBase { +export class AccountProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly accounts = new Set(); public pushOptions(options: IAccountProviderOptions): void { @@ -33,10 +35,14 @@ export class AccountProvider = Array> { - const result = { + public get baseStructure(): IAccountProviderData { + return { accounts: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const accounts = await data.get(AccountClassifier); for(const account of this.accounts) diff --git a/src/chain-observers/providers/alarm-provider.ts b/src/chain-observers/providers/alarm-provider.ts index ccfad42975908779e09a786db01257c4ee44a95c..c0a793ea9998b4ec9a3a4d92adf9224a18a992ff 100644 --- a/src/chain-observers/providers/alarm-provider.ts +++ b/src/chain-observers/providers/alarm-provider.ts @@ -26,7 +26,9 @@ export interface IAlarmProviderOptions { accounts: string[]; } -export class AlarmProvider = Array> extends ProviderBase { +export class AlarmProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly accounts = new Set(); public pushOptions(options: IAlarmProviderOptions): void { @@ -47,10 +49,14 @@ export class AlarmProvider = Array> { - const result: IAlarmAccountsData = { - alarmsPerAccount: {} as TAlarmAccounts + public get baseStructure(): IAlarmAccountsData { + return { + alarmsPerAccount: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const ensureHasAccount = (account: TAccountName) => { if (result.alarmsPerAccount[account] === undefined) diff --git a/src/chain-observers/providers/block-header-provider.ts b/src/chain-observers/providers/block-header-provider.ts index ac9f1e6db2de4cbccc0dbe403994a737b5cdc4ea..416d51c6af5f1bc3d3118feb82a53541a564e2d9 100644 --- a/src/chain-observers/providers/block-header-provider.ts +++ b/src/chain-observers/providers/block-header-provider.ts @@ -4,23 +4,27 @@ import { TProviderEvaluationContext } from "../factories/data-evaluation-context import { ProviderBase } from "./provider-base"; export interface IBlockHeaderProviderData { - block: IBlockHeaderData; + block?: IBlockHeaderData; }; -export class BlockHeaderProvider extends ProviderBase { +export class BlockHeaderProvider extends ProviderBase<{}, IBlockHeaderProviderData> { public usedContexts(): Array { return [ BlockHeaderClassifier ] } + public get baseStructure(): IBlockHeaderProviderData { + return {}; + } + public async provide(data: TProviderEvaluationContext): Promise { + const result = this.baseStructure; + const blockHeader = await data.get(BlockHeaderClassifier); - return { - block: { - ...blockHeader - } - }; + result.block = { ...blockHeader }; + + return result; } } diff --git a/src/chain-observers/providers/block-provider.ts b/src/chain-observers/providers/block-provider.ts index f04ac89a5f3a44c66a1d663cbeec8f531fbb8983..72a7650aa1e659d89c2350064cee5928aac3d4f7 100644 --- a/src/chain-observers/providers/block-provider.ts +++ b/src/chain-observers/providers/block-provider.ts @@ -5,10 +5,10 @@ import { TProviderEvaluationContext } from "../factories/data-evaluation-context import { ProviderBase } from "./provider-base"; export interface IBlockProviderData { - block: IBlockHeaderData & IBlockData; + block?: IBlockHeaderData & IBlockData; }; -export class BlockProvider extends ProviderBase { +export class BlockProvider extends ProviderBase<{}, IBlockProviderData> { public usedContexts(): Array { return [ BlockHeaderClassifier, @@ -16,15 +16,21 @@ export class BlockProvider extends ProviderBase { ] } + public get baseStructure(): IBlockProviderData { + return {}; + } + public async provide(data: TProviderEvaluationContext): Promise { + const result = this.baseStructure; + const blockHeader = await data.get(BlockHeaderClassifier); const block = await data.get(BlockClassifier); - return { - block: { - ...blockHeader, - ...block - } + result.block = { + ...blockHeader, + ...block }; + + return result; } } diff --git a/src/chain-observers/providers/blog-content-provider.ts b/src/chain-observers/providers/blog-content-provider.ts index 964dc89c8a2d0557112ed4bd7ce6a447057de4e2..d5ce94827d23aed8ed0ce0e720eadf416280397d 100644 --- a/src/chain-observers/providers/blog-content-provider.ts +++ b/src/chain-observers/providers/blog-content-provider.ts @@ -36,9 +36,10 @@ export interface IPostProviderData> { // Base class for blog content providers (posts and comments) export abstract class BlogContentProvider< + TStructure extends object = object, TAccounts extends Array = Array, TOptions extends object = object -> extends ProviderBase { +> extends ProviderBase { public readonly authors = new Map(); protected readonly isPost: boolean; @@ -91,7 +92,9 @@ export abstract class BlogContentProvider< } } -export class CommentProvider = Array> extends BlogContentProvider { +export class CommentProvider< + TAccounts extends Array = Array +> extends BlogContentProvider, TAccounts, ICommentProviderOptions> { public constructor() { super(false); // False = not posts, i.e., comments } @@ -101,27 +104,45 @@ export class CommentProvider = Array> { + public get baseStructure(): ICommentProviderData { return { - comments: await this.createProviderData(data) + comments: {} }; } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; + + result.comments = await this.createProviderData(data); + + return result; + } } -export class PostProvider = Array> extends BlogContentProvider { +export class PostProvider< + TAccounts extends Array = Array +> extends BlogContentProvider, TAccounts, IPostProviderOptions> { public constructor() { super(true); // True = posts } + public get baseStructure(): IPostProviderData { + return { + posts: {} + }; + } + public pushOptions(options: IPostProviderOptions): void { for (const account of options.authors) this.authors.set(account, undefined); } public async provide(data: TProviderEvaluationContext): Promise> { - return { - posts: await this.createProviderData(data) - }; + const result = this.baseStructure; + + result.posts = await this.createProviderData(data); + + return result; } } diff --git a/src/chain-observers/providers/content-metadata-provider.ts b/src/chain-observers/providers/content-metadata-provider.ts index 2b89abe03a47dd34c79d4a17226cd9dcf576436f..a112b65f99a38212ba8d27acc8d641b7cf685e9b 100644 --- a/src/chain-observers/providers/content-metadata-provider.ts +++ b/src/chain-observers/providers/content-metadata-provider.ts @@ -29,9 +29,10 @@ export interface IPostMetadataProviderData // Base class for blog content providers (posts and comments) export abstract class ContentMetadataProvider< + TStructure extends object = object, TAccounts extends Array = Array, TOptions extends object = object -> extends ProviderBase { +> extends ProviderBase { public readonly authors = new Set(); protected readonly isPost: boolean; @@ -68,7 +69,7 @@ export abstract class ContentMetadataProvider< } export class CommentMetadataProvider = Array> - extends ContentMetadataProvider { + extends ContentMetadataProvider, TAccounts, ICommentMetadataProviderOptions> { public constructor() { super(false); // False = not posts, i.e., comments } @@ -78,15 +79,23 @@ export class CommentMetadataProvider = Arr this.authors.add(account); } - public async provide(data: TProviderEvaluationContext): Promise> { + public get baseStructure(): ICommentMetadataProviderData { return { - commentsMetadata: await this.createProviderData(data) + commentsMetadata: {} }; } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; + + result.commentsMetadata = await this.createProviderData(data); + + return result; + } } export class PostMetadataProvider = Array> - extends ContentMetadataProvider { + extends ContentMetadataProvider, TAccounts, IPostMetadataProviderOptions> { public constructor() { super(true); // True = posts } @@ -96,10 +105,18 @@ export class PostMetadataProvider = Array< this.authors.add(account); } - public async provide(data: TProviderEvaluationContext): Promise> { + public get baseStructure(): IPostMetadataProviderData { return { - postsMetadata: await this.createProviderData(data) + postsMetadata: {} }; } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; + + result.postsMetadata = await this.createProviderData(data); + + return result; + } } diff --git a/src/chain-observers/providers/custom-operation-provider.ts b/src/chain-observers/providers/custom-operation-provider.ts index 5ccbdb5b1dd407cd2201824ed2451a4ce2a5916e..a7baadb63ab44e3128a27ac14c8995f8704d8f41 100644 --- a/src/chain-observers/providers/custom-operation-provider.ts +++ b/src/chain-observers/providers/custom-operation-provider.ts @@ -18,7 +18,7 @@ export interface ICustomOperationProviderOptions { export class CustomOperationProvider< TOperationId extends Array = Array -> extends ProviderBase { +> extends ProviderBase> { public readonly ids = new Set(); public pushOptions(options: ICustomOperationProviderOptions): void { @@ -30,10 +30,14 @@ export class CustomOperationProvider< return [OperationClassifier] } - public async provide(data: TProviderEvaluationContext): Promise> { - const result = { + public get baseStructure(): ICustomOperationProviderData { + return { customOperations: {} - } as ICustomOperationProviderData; + }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const accounts = await data.get(OperationClassifier); if (accounts.operationsPerType.custom_json_operation) diff --git a/src/chain-observers/providers/exchange-transfer-provider.ts b/src/chain-observers/providers/exchange-transfer-provider.ts index 586de3c8bdcc2009ee2995da0166ff84308811b0..059703c6849db5756bf0f6e8f667d79da4fef6c5 100644 --- a/src/chain-observers/providers/exchange-transfer-provider.ts +++ b/src/chain-observers/providers/exchange-transfer-provider.ts @@ -22,13 +22,19 @@ export interface IExchangeTransferProviderData { * This is because the escrow_transfer operation is used for both HIVE and HBD transfers. * If you want to extract the HIVE amount, you should extract it directly from the provided operations within transaction. */ -export class ExchangeTransferProvider extends ProviderBase { +export class ExchangeTransferProvider extends ProviderBase<{}, IExchangeTransferProviderData> { public usedContexts(): Array { return [ OperationClassifier ] } + public get baseStructure(): IExchangeTransferProviderData { + return { + exchangeTransferOperations: new WorkerBeeIterable([]) + }; + } + public async provide(data: TProviderEvaluationContext): Promise { const operations = await data.get(OperationClassifier); @@ -107,8 +113,10 @@ export class ExchangeTransferProvider extends ProviderBase { }); } - return { - exchangeTransferOperations: new WorkerBeeIterable(exchangeTransfers) - }; + const result = this.baseStructure; + + result.exchangeTransferOperations = new WorkerBeeIterable(exchangeTransfers); + + return result; } } diff --git a/src/chain-observers/providers/feed-price-provider.ts b/src/chain-observers/providers/feed-price-provider.ts index ed40a5c3799ded7a4a00168537df7243cc642217..20cd3be55600c7750945c0006a0962e256d71d05 100644 --- a/src/chain-observers/providers/feed-price-provider.ts +++ b/src/chain-observers/providers/feed-price-provider.ts @@ -13,26 +13,32 @@ export interface IFeedPriceData { } export interface IFeedPriceProviderData { - feedPrice: IFeedPriceData; + feedPrice?: IFeedPriceData; }; -export class FeedPriceProvider extends ProviderBase { +export class FeedPriceProvider extends ProviderBase<{}, IFeedPriceProviderData> { public usedContexts(): Array { return [ FeedPriceClassifier ] } + public get baseStructure(): IFeedPriceProviderData { + return {}; + } + public async provide(data: TProviderEvaluationContext): Promise { + const result = this.baseStructure; + const { currentMedianHistory, currentMinHistory, currentMaxHistory, priceHistory } = await data.get(FeedPriceClassifier); - return { - feedPrice: { - currentMedianHistory, - currentMinHistory, - currentMaxHistory, - priceHistory: new WorkerBeeIterable(priceHistory) - } + result.feedPrice = { + currentMedianHistory, + currentMinHistory, + currentMaxHistory, + priceHistory: new WorkerBeeIterable(priceHistory) }; + + return result; } } diff --git a/src/chain-observers/providers/follow-provider.ts b/src/chain-observers/providers/follow-provider.ts index a3b2b84541b6b9282e9fac7cd09bb01842dc4778..a50c106dae825772d5e8035acff893b87efe9685 100644 --- a/src/chain-observers/providers/follow-provider.ts +++ b/src/chain-observers/providers/follow-provider.ts @@ -22,7 +22,9 @@ export interface IFollowProviderOptions { accounts: TAccountName[]; } -export class FollowProvider = Array> extends ProviderBase { +export class FollowProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly accounts = new Set(); public pushOptions(options: IFollowProviderOptions): void { @@ -34,10 +36,14 @@ export class FollowProvider = Array> { - const result = { + public get baseStructure(): IFollowProviderData { + return { follows: {} - } as IFollowProviderData; + }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const accounts = await data.get(OperationClassifier); if (accounts.operationsPerType.custom_json_operation) diff --git a/src/chain-observers/providers/impacted-account-provider.ts b/src/chain-observers/providers/impacted-account-provider.ts index 5d26f2e4aa4cb4f2efce2b83e62c6fbde45f5f2a..02ef49b764eb056812ff33e1396983118ec4a5ef 100644 --- a/src/chain-observers/providers/impacted-account-provider.ts +++ b/src/chain-observers/providers/impacted-account-provider.ts @@ -18,7 +18,9 @@ export interface IImpactedAccountProviderOptions { accounts: string[]; } -export class ImpactedAccountProvider = Array> extends ProviderBase { +export class ImpactedAccountProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly accounts = new Set(); public pushOptions(options: IImpactedAccountProviderOptions): void { @@ -32,10 +34,14 @@ export class ImpactedAccountProvider = Arr ]; } - public async provide(data: TProviderEvaluationContext): Promise> { - const result = { + public get baseStructure(): IImpactedAccountProviderData { + return { impactedAccounts: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const { impactedAccounts } = await data.get(ImpactedAccountClassifier); for(const account of this.accounts) diff --git a/src/chain-observers/providers/internal-market-provider.ts b/src/chain-observers/providers/internal-market-provider.ts index 48bf169f6aa9e6cf7a1c330ae614bd68c11f4ceb..146b4092c8baf272f0a11055e075757c6835898f 100644 --- a/src/chain-observers/providers/internal-market-provider.ts +++ b/src/chain-observers/providers/internal-market-provider.ts @@ -28,13 +28,19 @@ export interface IInternalMarketProviderData { internalMarketOperations: WorkerBeeIterable>; }; -export class InternalMarketProvider extends ProviderBase { +export class InternalMarketProvider extends ProviderBase<{}, IInternalMarketProviderData> { public usedContexts(): Array { return [ OperationClassifier ] } + public get baseStructure(): IInternalMarketProviderData { + return { + internalMarketOperations: new WorkerBeeIterable([]) + }; + } + public async provide(data: TProviderEvaluationContext): Promise { const operations = await data.get(OperationClassifier); @@ -88,8 +94,10 @@ export class InternalMarketProvider extends ProviderBase { transaction: op.transaction }); - return { - internalMarketOperations: new WorkerBeeIterable(internalMarketOperations) - }; + const result = this.baseStructure; + + result.internalMarketOperations = new WorkerBeeIterable(internalMarketOperations); + + return result; } } diff --git a/src/chain-observers/providers/manabar-provider.ts b/src/chain-observers/providers/manabar-provider.ts index d44addc4253edafabc3605699e4e6d2ccdccc773..6e16487d863bb49d118dad841d4fa397f364311c 100644 --- a/src/chain-observers/providers/manabar-provider.ts +++ b/src/chain-observers/providers/manabar-provider.ts @@ -16,7 +16,9 @@ export interface IManabarProviderOptions { manabarData: IManabarCollectorOptions[]; } -export class ManabarProvider = Array> extends ProviderBase { +export class ManabarProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly manabarData = new Map>(); public pushOptions(options: IManabarProviderOptions): void { @@ -38,10 +40,14 @@ export class ManabarProvider = Array> { - const result = { + public get baseStructure(): IManabarProviderData { + return { manabarData: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const { manabarData } = await data.get(ManabarClassifier); for(const [account, manabarTypes] of this.manabarData) { diff --git a/src/chain-observers/providers/mention-provider.ts b/src/chain-observers/providers/mention-provider.ts index 36910d6dcc35285c81765c1dd9263434b2d94568..55f50c8f601b8bfdc9ef73b006265c21cd9a84d0 100644 --- a/src/chain-observers/providers/mention-provider.ts +++ b/src/chain-observers/providers/mention-provider.ts @@ -17,7 +17,9 @@ export interface IMentionedAccountProviderOptions { accounts: string[]; } -export class MentionedAccountProvider = Array> extends ProviderBase { +export class MentionedAccountProvider< + TMentions extends Array = Array +> extends ProviderBase> { public readonly accounts = new Set(); public pushOptions(options: IMentionedAccountProviderOptions): void { @@ -31,8 +33,14 @@ export class MentionedAccountProvider = Ar ]; } + public get baseStructure(): IMentionedAccountProviderData { + return { + mentioned: {} + }; + } + public async provide(data: TProviderEvaluationContext): Promise> { - const mentioned = {} as IMentionedAccountProviderData["mentioned"]; + const result = this.baseStructure; const { operationsPerType } = await data.get(OperationClassifier); @@ -53,20 +61,18 @@ export class MentionedAccountProvider = Ar while ((match = mentionRegex.exec(operation.body)) !== null && !foundMention) { const mentionedAccount = match[1] as TAccountName; if (this.accounts.has(mentionedAccount)) { - if (!mentioned[mentionedAccount]) - mentioned[mentionedAccount] = []; + if (!result.mentioned[mentionedAccount]) + result.mentioned[mentionedAccount] = []; - mentioned[mentionedAccount].push(operation); + result.mentioned[mentionedAccount].push(operation); foundMention = true; } } } - for(const account in mentioned) - mentioned[account] = new WorkerBeeIterable(mentioned[account]); + for(const account in result.mentioned) + result.mentioned[account] = new WorkerBeeIterable(result.mentioned[account]); - return { - mentioned - } as IMentionedAccountProviderData; + return result; } } diff --git a/src/chain-observers/providers/new-account-provider.ts b/src/chain-observers/providers/new-account-provider.ts index db53b9ed53cc17f34e72b15341cc66014b6d4fe6..b5a32de182b44e6a6154759d1a2431c492ea6d7b 100644 --- a/src/chain-observers/providers/new-account-provider.ts +++ b/src/chain-observers/providers/new-account-provider.ts @@ -19,13 +19,19 @@ export interface INewAccountProviderData { newAccounts: WorkerBeeIterable; }; -export class NewAccountProvider extends ProviderBase { +export class NewAccountProvider extends ProviderBase<{}, INewAccountProviderData> { public usedContexts(): Array { return [OperationClassifier]; } + public get baseStructure(): INewAccountProviderData { + return { + newAccounts: new WorkerBeeIterable([]) + }; + } + public async provide(data: TProviderEvaluationContext): Promise { - const result: TNewAccountProvided[] = []; + const newAccounts: TNewAccountProvided[] = []; const { operationsPerType } = await data.get(OperationClassifier); @@ -43,7 +49,7 @@ export class NewAccountProvider extends ProviderBase { // eslint-disable-next-line no-empty } catch {} - result.push({ + newAccounts.push({ accountName: operation.new_account_name, active: operation.active!, creator: operation.creator, @@ -54,8 +60,10 @@ export class NewAccountProvider extends ProviderBase { }); } - return { - newAccounts: new WorkerBeeIterable(result) - } as INewAccountProviderData; + const result = this.baseStructure; + + result.newAccounts = new WorkerBeeIterable(newAccounts); + + return result; } } diff --git a/src/chain-observers/providers/provider-base.ts b/src/chain-observers/providers/provider-base.ts index d5f752253c887782025ca8331875b2e5183b20f5..bf2d8c5046277072879b9be3e9abc88f3e621197 100644 --- a/src/chain-observers/providers/provider-base.ts +++ b/src/chain-observers/providers/provider-base.ts @@ -1,10 +1,20 @@ import { TRegisterEvaluationContext } from "../classifiers/collector-classifier-base"; import { TProviderEvaluationContext } from "../factories/data-evaluation-context"; -export abstract class ProviderBase { +export abstract class ProviderBase { public abstract usedContexts(): Array; public pushOptions?(options: IOptions): void; public abstract provide(data: TProviderEvaluationContext): Promise; + + /** + * The base structure of the data provided by this provider. + * + * This can be used for type checking and ensuring that the data provided by the provider adheres to a specific structure. + * This is also called as a fallback when no data is provided by the provider, e.g. due to an error. + * + * Note: This is intentionally a getter to avoid issues with shared state between multiple #provide calls + */ + public abstract get baseStructure(): Structure; } diff --git a/src/chain-observers/providers/rc-account-provider.ts b/src/chain-observers/providers/rc-account-provider.ts index eb3696554bce6b5fd0c90085d916782141938224..2bb55ed543bc9e7b34a7abf41099ccf142892a3c 100644 --- a/src/chain-observers/providers/rc-account-provider.ts +++ b/src/chain-observers/providers/rc-account-provider.ts @@ -16,7 +16,9 @@ export interface IRcAccountsProviderOptions { accounts: string[]; } -export class RcAccountProvider = Array> extends ProviderBase { +export class RcAccountProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly rcAccounts = new Set(); public pushOptions(options: IRcAccountsProviderOptions): void { @@ -32,10 +34,14 @@ export class RcAccountProvider = Array> { - const result = { + public get baseStructure(): IRcAccountProviderData { + return { rcAccounts: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const rcAccounts = await data.get(RcAccountClassifier); for(const rcAccount of this.rcAccounts) diff --git a/src/chain-observers/providers/reblog-provider.ts b/src/chain-observers/providers/reblog-provider.ts index 822521ec5079bbdf1b0f8dfece8c34c80c09243e..d59870f05f118a428e0c5b1bbab1b3e2607ab93a 100644 --- a/src/chain-observers/providers/reblog-provider.ts +++ b/src/chain-observers/providers/reblog-provider.ts @@ -23,7 +23,9 @@ export interface IReblogProviderOptions { accounts: TAccountName[]; } -export class ReblogProvider = Array> extends ProviderBase { +export class ReblogProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly accounts = new Set(); public pushOptions(options: IReblogProviderOptions): void { @@ -35,10 +37,14 @@ export class ReblogProvider = Array> { - const result = { + public get baseStructure(): IReblogProviderData { + return { reblogs: {} - } as IReblogProviderData; + }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const accounts = await data.get(OperationClassifier); if (accounts.operationsPerType.custom_json_operation) diff --git a/src/chain-observers/providers/transaction-provider.ts b/src/chain-observers/providers/transaction-provider.ts index adadba921b7c73564af65729892b4e27944aeeb9..523b1c480fcd4bd061c62fb57165813bb9f1a9e9 100644 --- a/src/chain-observers/providers/transaction-provider.ts +++ b/src/chain-observers/providers/transaction-provider.ts @@ -16,7 +16,9 @@ export interface ITransactionByIdProviderOptions { transactionIds: string[]; } -export class TransactionByIdProvider = Array> extends ProviderBase { +export class TransactionByIdProvider< + TIdOfTx extends Array = Array +> extends ProviderBase> { public readonly transactionIds = new Set(); public pushOptions(options: ITransactionByIdProviderOptions): void { @@ -30,10 +32,14 @@ export class TransactionByIdProvider = Array> { - const result = { + public get baseStructure(): ITransactionProviderData { + return { transactions: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const block = await data.get(BlockClassifier); for(const txId of this.transactionIds) diff --git a/src/chain-observers/providers/vote-provider.ts b/src/chain-observers/providers/vote-provider.ts index 07267f123264c15d964bbab75d750750d519d017..5970a3e33464c31f013349838b002d42ea2e69d8 100644 --- a/src/chain-observers/providers/vote-provider.ts +++ b/src/chain-observers/providers/vote-provider.ts @@ -17,7 +17,9 @@ export interface IVoteProviderOptions { voters: string[]; } -export class VoteProvider = Array> extends ProviderBase { +export class VoteProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly voters = new Set(); public pushOptions(options: IVoteProviderOptions): void { @@ -29,10 +31,14 @@ export class VoteProvider = Array> { - const result = { + public get baseStructure(): IVoteProviderData { + return { votes: {} - } as IVoteProviderData; + }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const operations = await data.get(OperationClassifier); if (operations.operationsPerType.vote_operation) diff --git a/src/chain-observers/providers/whale-alert-provider.ts b/src/chain-observers/providers/whale-alert-provider.ts index c49ec38f2efc50aa5bc987307fa782aad36da871..8ba5b0b5e0ed16e990ed97ee2f7eb7543863a48b 100644 --- a/src/chain-observers/providers/whale-alert-provider.ts +++ b/src/chain-observers/providers/whale-alert-provider.ts @@ -20,7 +20,7 @@ export interface IWhaleAlertProviderOptions { assets: asset[]; } -export class WhaleAlertProvider extends ProviderBase { +export class WhaleAlertProvider extends ProviderBase { public readonly assets = new Map(); public pushOptions(options: IWhaleAlertProviderOptions): void { @@ -34,6 +34,12 @@ export class WhaleAlertProvider extends ProviderBase ] } + public get baseStructure(): IWhaleAlertProviderData { + return { + whaleOperations: new WorkerBeeIterable([]) + }; + } + public async provide(data: TProviderEvaluationContext): Promise { const operations = await data.get(OperationClassifier); @@ -103,8 +109,10 @@ export class WhaleAlertProvider extends ProviderBase }); } - return { - whaleOperations: new WorkerBeeIterable(whaleOperations) - }; + const result = this.baseStructure; + + result.whaleOperations = new WorkerBeeIterable(whaleOperations); + + return result; } } diff --git a/src/chain-observers/providers/witness-provider.ts b/src/chain-observers/providers/witness-provider.ts index aad3d79fc50f901f6aca86068958832bc53f5986..940f5e0c90c7c1f7fee107664e0e9f5b81365ff8 100644 --- a/src/chain-observers/providers/witness-provider.ts +++ b/src/chain-observers/providers/witness-provider.ts @@ -16,7 +16,9 @@ export interface IWitnessProviderOptions { accounts: string[]; } -export class WitnessProvider = Array> extends ProviderBase { +export class WitnessProvider< + TAccounts extends Array = Array +> extends ProviderBase> { public readonly witnesses = new Set(); public pushOptions(options: IWitnessProviderOptions): void { @@ -32,10 +34,14 @@ export class WitnessProvider = Array> { - const result = { + public get baseStructure(): IWitnessProviderData { + return { witnesses: {} }; + } + + public async provide(data: TProviderEvaluationContext): Promise> { + const result = this.baseStructure; const { witnesses } = await data.get(WitnessClassifier); for(const witness of this.witnesses) diff --git a/src/queen.ts b/src/queen.ts index 274f1cb9de1d4bced4b1c5380751cb69125c49a0..9c9e4e7e15b8511dbf14281070c4fc1e977a1a2c 100644 --- a/src/queen.ts +++ b/src/queen.ts @@ -6,6 +6,7 @@ import { AccountFullManabarFilter } from "./chain-observers/filters/account-full import { AccountMetadataChangeFilter } from "./chain-observers/filters/account-metadata-change-filter"; import { AlarmFilter } from "./chain-observers/filters/alarm-filter"; import { BalanceChangeFilter } from "./chain-observers/filters/balance-change-filter"; +import { BlankFilter } from "./chain-observers/filters/blank-filter"; import { BlockNumberFilter } from "./chain-observers/filters/block-filter"; import { CommentFilter, PostFilter } from "./chain-observers/filters/blog-content-filter"; import { LogicalAndFilter, LogicalOrFilter } from "./chain-observers/filters/composite-filter"; @@ -96,6 +97,10 @@ export class QueenBee { this.applyAnd(); const committedFilters = this.filterContainers; + // If no filters are committed, add a blank filter which always evaluates to true, so users can receive data from providers only + if (committedFilters.length === 0) + committedFilters.push(new BlankFilter(this.worker)); + // Optimize by not creating a logical AND filter for only one filter const andFilter: FilterBase = committedFilters.length === 1 ? committedFilters[0] : new LogicalAndFilter(this.worker, committedFilters); @@ -319,9 +324,9 @@ export class QueenBee { * next: (data) => { * console.log("username account balance changed"); * console.log("Account data:", data.accounts["username"]); - * console.log("HIVE balance:", data.accounts["username"].balance.HIVE); - * console.log("HBD balance:", data.accounts["username"].balance.HBD); - * console.log("HP balance:", data.accounts["username"].balance.HP); + * console.log("HIVE balance:", data.accounts["username"]?.balance.HIVE); + * console.log("HBD balance:", data.accounts["username"]?.balance.HBD); + * console.log("HP balance:", data.accounts["username"]?.balance.HP); * } * }); * ``` @@ -351,8 +356,8 @@ export class QueenBee { * next: (data) => { * console.log("Account username metadata changed"); * console.log("Account data:", data.accounts["username"]); - * console.log("JSON metadata:", data.accounts["username"].jsonMetadata); - * console.log("Posting JSON metadata:", data.accounts["username"].postingJsonMetadata); + * console.log("JSON metadata:", data.accounts["username"]?.jsonMetadata); + * console.log("Posting JSON metadata:", data.accounts["username"]?.postingJsonMetadata); * } * }); * ``` @@ -576,8 +581,9 @@ export class QueenBee { * ```ts * workerbee.observe.onCustomOperation("sm_claim_reward").subscribe({ * next: (data) => { - * for(const { operation } of data.customOperations["sm_claim_reward"]) - * console.log("Splinterlands reward claimed:", operation); + * if (data.customOperations["sm_claim_reward"] !== undefined) + * for(const { operation } of data.customOperations["sm_claim_reward"]) + * console.log("Splinterlands reward claimed:", operation); * } * }); * ``` @@ -667,8 +673,9 @@ export class QueenBee { * ```ts * workerbee.observe.onReblog("username").subscribe({ * next: (data) => { - * for(const { operation } of data.reblogs["username"]) - * console.log("Post reblogged:", operation); + * if (data.reblogs["username"] !== undefined) + * for(const { operation } of data.reblogs["username"]) + * console.log("Post reblogged:", operation); * } * }); * ``` @@ -696,8 +703,9 @@ export class QueenBee { * ```ts * workerbee.observe.onFollow("trustworthy.account").subscribe({ * next: (data) => { - * for(const { operation } of data.reblogs["trustworthy.account"]) - * console.log("trustworthy.account followed:", operation); + * if (data.reblogs["trustworthy.account"] !== undefined) + * for(const { operation } of data.reblogs["trustworthy.account"]) + * console.log("trustworthy.account followed:", operation); * } * }); * ``` @@ -725,8 +733,9 @@ export class QueenBee { * ```ts * workerbee.observe.onMention("username").subscribe({ * next: (data) => { - * for(const operation of data.mentioned["username"]) - * console.log("username mentioned in post:", operation); + * if (data.mentioned["username"] !== undefined) + * for(const operation of data.mentioned["username"]) + * console.log("username mentioned in post:", operation); * } * }); * ``` @@ -755,8 +764,9 @@ export class QueenBee { * ```ts * workerbee.observe.onAlarm("username").subscribe({ * next: (data) => { - * for(const alarmType of data.alarmsPerAccount["username"]) - * console.log("username account alarm!:", alarmType); + * if (data.alarmsPerAccount["username"] !== undefined) + * for(const alarmType of data.alarmsPerAccount["username"]) + * console.log("username account alarm!:", alarmType); * } * }); * ``` @@ -868,11 +878,18 @@ export class QueenBee { * * Automatically provides the block header data in the `next` callback. * + * Note: `data.block` can be undefined in some cases, e.g. when you applied advanced filter logic in your code, + * one of the execution paths evaluated to true, but block provider could not retrieve the block data from the Node API + * due to an error. That's why it's always a good practice to check if `data.block` is defined before accessing its + * properties with advanced filters. In the example above we used `!` as a TypeScript-only feature - Non-Null Assertions, + * which tells the compiler that we are sure that `data.block` is not null or undefined. + * In the example with just the {@link onBlock} filter, the block data should always be defined. + * * @example * ```ts * workerbee.observe.onBlock().subscribe({ * next: (data) => { - * console.log("New block detected:", data.block.number); + * console.log("New block detected:", data.block!.number); * } * }); * ``` @@ -1015,11 +1032,18 @@ export class QueenBee { /** * Provides block header data in the `next` callback. * + * Note: `data.block` can be undefined in some cases, e.g. when you applied advanced filter logic in your code, + * one of the execution paths evaluated to true, but block provider could not retrieve the block data from the Node API + * due to an error. That's why it's always a good practice to check if `data.block` is defined before accessing its + * properties with advanced filters. In the example above we used `!` as a TypeScript-only feature - Non-Null Assertions, + * which tells the compiler that we are sure that `data.block` is not null or undefined. + * In the example with just the {@link onBlock} filter, the block data should always be defined. + * * @example * ```ts * workerbee.observe.onBlock().provideBlockHeaderData().subscribe({ * next: (data) => { - * console.log("New block detected:", data.block.number); + * console.log("New block detected:", data.block!.number); * } * }); * ``` @@ -1039,7 +1063,8 @@ export class QueenBee { * ```ts * workerbee.observe.onBlock().provideManabarData(EManabarType.RC, "username1", "username2").subscribe({ * next: (data) => { - * console.log("Account manabar is now loaded %:", data.manabarData["username1"][EManabarType.RC].percent); + * if (data.manabarData["username1"] !== undefined) + * console.log("Account manabar is now loaded %:", data.manabarData["username1"][EManabarType.RC].percent); * } * }); * ``` @@ -1064,12 +1089,19 @@ export class QueenBee { * * Automatically provides both block header and block data. * + * Note: `data.block` can be undefined in some cases, e.g. when you applied advanced filter logic in your code, + * one of the execution paths evaluated to true, but block provider could not retrieve the block data from the Node API + * due to an error. That's why it's always a good practice to check if `data.block` is defined before accessing its + * properties with advanced filters. In the example above we used `!` as a TypeScript-only feature - Non-Null Assertions, + * which tells the compiler that we are sure that `data.block` is not null or undefined. + * In the example with just the {@link onBlock} filter, the block data should always be defined. + * * @example * ```ts * workerbee.observe.onBlock().provideBlockData().subscribe({ * next: (data) => { - * console.log("New block detected:", data.block.number); - * console.log("Block transactions:", data.block.transactions); + * console.log("New block detected:", data.block!.number); + * console.log("Block transactions:", data.block!.transactions); * } * }); * ```