Skip to content

improve async node flattening and apply resolver cache to async nodes#694

Merged
tmarmer merged 20 commits intomainfrom
async-node-flattening
Aug 15, 2025
Merged

improve async node flattening and apply resolver cache to async nodes#694
tmarmer merged 20 commits intomainfrom
async-node-flattening

Conversation

@tmarmer
Copy link
Copy Markdown
Contributor

@tmarmer tmarmer commented Jul 28, 2025

Change Type (required)

Indicate the type of change your pull request is:

  • patch
  • minor
  • major
  • N/A

Does your PR have any documentation updates?

  • Updated docs
  • No Update needed
  • Unable to update docs

Release Notes

  • Simplify async node flattening to just check for multi-nodes inside multi-nodes after async nodes are resolved.
  • Introduce async node id cache in the resolver to allow async nodes to be marked as dirty and limit scope of resolver update to the specific node and use cache for the rest of the update.
📦 Published PR as canary version: 0.12.1--canary.694.25095

Try this version out locally by upgrading relevant packages to 0.12.1--canary.694.25095

Comment thread plugins/async-node/core/src/transform.ts Outdated
@tmarmer tmarmer force-pushed the async-node-flattening branch from 33236d4 to c45c574 Compare July 29, 2025 14:36
@codecov
Copy link
Copy Markdown

codecov Bot commented Jul 29, 2025

Codecov Report

❌ Patch coverage is 98.01980% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.63%. Comparing base (a22123b) to head (30a7f51).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ns/async-node/core/src/utils/traverseAndReplace.ts 86.20% 4 Missing ⚠️
...lugins/async-node/core/src/createAsyncTransform.ts 97.91% 1 Missing ⚠️
plugins/async-node/core/src/index.ts 98.52% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #694      +/-   ##
==========================================
+ Coverage   90.32%   90.63%   +0.30%     
==========================================
  Files         336      341       +5     
  Lines       21258    21409     +151     
  Branches     2128     2173      +45     
==========================================
+ Hits        19202    19404     +202     
+ Misses       2038     1989      -49     
+ Partials       18       16       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tmarmer tmarmer force-pushed the async-node-flattening branch from 331debd to 6cd630d Compare July 30, 2025 16:20
@tmarmer
Copy link
Copy Markdown
Contributor Author

tmarmer commented Jul 30, 2025

/canary

@tmarmer tmarmer force-pushed the async-node-flattening branch from 6551459 to c3bb8d1 Compare August 6, 2025 18:26
@tmarmer tmarmer marked this pull request as ready for review August 6, 2025 18:26
@tmarmer tmarmer requested a review from a team as a code owner August 6, 2025 18:26
### AsyncTransform
`createAsyncTransform` is a helper function to create the transform for async asset. The function takes a set of options that help adjust the transform generated to your needs:
- flatten (Optional. Default: true) - Whether or not to flatten chained results of the same asset type into a single container.
- path (Optional. Default: ["values"]) - The path to the array within the `wrapperAssetType` that will contain the async content.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the benefit of giving this as an option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path option here is to allow for cases where maybe the warpperAssetType wants its values somewhere else. The best example I can think of would be if someone had something like a table asset with rows instead of values as the path.

- transformAssetType (Required) - The asset type that the transform is matching against.
- wrapperAssetType (Required) - The asset type that will contain the async content.
- getNestedAsset (Optional) - Function to get any nested asset that will need to be extracted and kept when creating the wrapper asset. This is useful when combined with `flatten: true` to help chain async content with a single asset type.
- getAsyncNodeId (Optional) - Function to get the id for the generated async node. By default it uses a format of `async-{asset.id}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥


const asyncTransform = (node: Node.ViewOrAsset) => {
const id = getAsyncNodeId(node);
const asset = getNestedAsset?.(node);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe i'm not understanding this properly, the usage of this function is weird. This is entrusting the consumers to pass in a proper getNestedAsset function so we can use it to retrieve the nested asset in our multi-node?

should we not be the one doing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getNestedAsset replaces what the asset argument in the original asyncTransform did. Since we need to repeat it here I decided to use a function. We could alternatively take a nestedAssetPath that defaults to value and get it ourselves I think. Pretty much, this allows for transforms other than our chat-message use case where maybe the chained assets are somewhere else.

Comment thread plugins/async-node/core/src/__tests__/createAsyncTransform.test.ts Outdated
Comment on lines +57 to +64
return childOptions === undefined
? multiNode
: [
{
path: [...childOptions.path, childOptions.key],
value: multiNode,
},
];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in here allow multi nodes to be parsed if they are the root object. This is to allow async node results to return arrays of data that can either be used to flatten into a higher level multi-node or just to produce a multi node for some property.

Comment on lines +167 to +176
asyncChanges?.forEach((id) => {
let current: Node.Node | undefined = prevAsyncIdMap.get(id);
while (current && prevASTMap.has(current)) {
const next = prevASTMap.get(current);
if (next && this.resolveCache.has(next)) {
this.resolveCache.delete(next);
}
current = current.parent;
}
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setup here lets us clear some of the cache when an async node updates. I wonder if we can use something similar to the dependency tracker used for data changes here instead?

Comment on lines +365 to +366
for (const id of resolvedAST.asyncNodesResolved ?? []) {
nextAsyncIdMap.set(id, resolvedAST);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of asyncNodesResolved is a special case made necessary by some of the changes to how flattening is handled for async nodes. Because these nodes are resolved by and flattened when the parent node is doing onBeforeResolve, the parent needs to be aware of the async ids that were resolved so it can have its cache cleared if that node updates.

Comment thread core/player/src/view/resolver/index.ts Outdated
.filter((index) => index !== -1);

const newValues = resolvedAST.values.map((mValue) => {
resolvedAST.values = resolvedAST.values.flatMap((mValue) => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here and below are just removing some of the flattening work since it's handled by the async node plugin now.

Comment on lines +112 to +113
public updateAsync(asyncNode: string): void {
const update = this.resolver?.update(new Set(), new Set([asyncNode]));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't used undefined for the data changes since that clears the whole cache.


const asyncTransform = (node: Node.ViewOrAsset) => {
const id = getAsyncNodeId(node);
const asset = getNestedAsset?.(node);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getNestedAsset replaces what the asset argument in the original asyncTransform did. Since we need to repeat it here I decided to use a function. We could alternatively take a nestedAssetPath that defaults to value and get it ourselves I think. Pretty much, this allows for transforms other than our chat-message use case where maybe the chained assets are somewhere else.

### AsyncTransform
`createAsyncTransform` is a helper function to create the transform for async asset. The function takes a set of options that help adjust the transform generated to your needs:
- flatten (Optional. Default: true) - Whether or not to flatten chained results of the same asset type into a single container.
- path (Optional. Default: ["values"]) - The path to the array within the `wrapperAssetType` that will contain the async content.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path option here is to allow for cases where maybe the warpperAssetType wants its values somewhere else. The best example I can think of would be if someone had something like a table asset with rows instead of values as the path.

@KetanReddy KetanReddy added the minor Increment the minor version when merged label Aug 13, 2025
wrapperAssetType,
asset,
flatten,
flatten = true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this backwards compatible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, no, I meant to undo this after moving stuff to the createAsyncTransform. Good catch, will remove.

@tmarmer tmarmer force-pushed the async-node-flattening branch from 185649d to 9a9ee75 Compare August 14, 2025 18:19
KetanReddy
KetanReddy previously approved these changes Aug 14, 2025
@tmarmer tmarmer merged commit 14177bb into main Aug 15, 2025
12 checks passed
@tmarmer tmarmer deleted the async-node-flattening branch August 15, 2025 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants