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

Example asset and transform for async content streaming #575

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6e1ec87
example asset and transform for async content streaming
cehan-Chloe Jan 16, 2025
6b10342
async node builder test
cehan-Chloe Jan 16, 2025
c582503
update wrapper component
cehan-Chloe Jan 20, 2025
964cc11
add transform util func to async-node-plugin
cehan-Chloe Jan 30, 2025
9ba910c
chat-message-wrapper transform
cehan-Chloe Jan 30, 2025
a55bcb8
add missing type
cehan-Chloe Jan 30, 2025
e5a2bc4
remove flatten from multinode
cehan-Chloe Jan 30, 2025
ab3249e
use collection as wrapper for transformed chat-message asset
cehan-Chloe Feb 11, 2025
b304e85
clean up chat-message-wrapper
cehan-Chloe Feb 12, 2025
2a48a4f
snowflake-id
cehan-Chloe Feb 12, 2025
8f213b5
move module declaration to typings
cehan-Chloe Feb 12, 2025
97bf9be
add flatten and tests
cehan-Chloe Feb 14, 2025
a3ca873
add flatten
cehan-Chloe Feb 18, 2025
c276791
clean up comments
cehan-Chloe Feb 18, 2025
974dd23
remove flatten from multi-node
cehan-Chloe Feb 18, 2025
4402f56
remove player/react dependency for asyncNodePlugin
cehan-Chloe Feb 18, 2025
c672fe8
add chained chat-message test
cehan-Chloe Feb 18, 2025
0f1a413
update doc and tests based on review comments
cehan-Chloe Feb 20, 2025
8028d84
fix eslint
cehan-Chloe Feb 20, 2025
6d42eb8
add asyncTransform test
cehan-Chloe Feb 24, 2025
59181a1
DSL mock
cehan-Chloe Feb 26, 2025
7475f30
Async streaming android
sakuntala-motukuri Feb 21, 2025
dd2e557
Fixed linter issue for ChatMessageAssetTest.kt
sakuntala-motukuri Feb 21, 2025
f0a9eca
Added chained and singular resolve of chat-message asset in the node …
sakuntala-motukuri Feb 24, 2025
6246b85
Updated tests for chat-message asset singular and chained by making i…
sakuntala-motukuri Feb 25, 2025
ee7c233
Updated code with a ToDo
sakuntala-motukuri Feb 26, 2025
d48bd95
Changed async node resolve with singular and chained chat -message tests
sakuntala-motukuri Feb 27, 2025
0d736a3
fix dsl validator test failure
cehan-Chloe Feb 27, 2025
1075fa9
Applied DSL mock content to android
sakuntala-motukuri Feb 27, 2025
3b43b91
Merge branch 'async-streaming-core' into async-streaming-player-android
sakuntala-motukuri Feb 27, 2025
7828cb2
async-node doc update
cehan-Chloe Feb 27, 2025
c8c908c
Merge branch 'async-streaming-core' into async-streaming-player-android
sakuntala-motukuri Feb 27, 2025
cf2147a
async streaming ios update
cehan-Chloe Feb 21, 2025
3bcae32
add callback test
cehan-Chloe Feb 26, 2025
60e5c52
update content
cehan-Chloe Feb 27, 2025
7fc1607
Merge pull request #600 from player-ui/async-streaming-player-android
sakuntala-motukuri Feb 28, 2025
b116d7d
Merge pull request #599 from player-ui/async-streaming-ios
cehan-Chloe Feb 28, 2025
8fc5767
allow wrapInAsset
cehan-Chloe Feb 28, 2025
5d07bb8
fix dsl type validation
cehan-Chloe Feb 28, 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
38 changes: 38 additions & 0 deletions core/player/src/view/builder/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,44 @@ describe("multiNode", () => {
expect(v1.parent).toBe(result);
expect(v2.parent).toBe(result);
});

test("multinode with async node", () => {
const v1 = Builder.asyncNode("1");
const v2 = Builder.asyncNode("2");
const result = Builder.multiNode(v1, v2);

expect(result.type).toBe(NodeType.MultiNode);
expect(result.values[0]?.type).toBe("async");
expect(result.values[1]?.type).toBe("async");
expect(v1.parent).toBe(result);
expect(v2.parent).toBe(result);
});
});

test("async node", () => {
const result = Builder.asyncNode("1", false);
expect(result.type).toBe(NodeType.Async);
expect(result.id).toBe("1");
expect(result.flatten).toBe(false);

const result2 = Builder.asyncNode("2");
expect(result2.type).toBe(NodeType.Async);
expect(result2.id).toBe("2");
expect(result2.flatten).toBe(true);
});

test("asset wrapper", () => {
const result = Builder.assetWrapper({
type: NodeType.Asset,
value: {
id: "1",
type: "text",
value: "chat message",
},
});

expect(result.type).toBe(NodeType.Value);
expect(result.children?.[0]?.value.type).toBe("asset");
});

describe("addChild", () => {
Expand Down
29 changes: 27 additions & 2 deletions core/player/src/view/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export class Builder {
};
}

static assetWrapper<T extends Node.Node>(value: T): Node.Value {
const valueNode = Builder.value();
Builder.addChild(valueNode, "asset", value);
return valueNode;
}

/**
* Creates a value node
*
Expand All @@ -33,10 +39,10 @@ export class Builder {
* Creates a multiNode and associates the multiNode as the parent
* of all the value nodes
*
* @param values - the value or applicability nodes to put in the multinode
* @param values - the value, applicability or async nodes to put in the multinode
*/
static multiNode(
...values: (Node.Value | Node.Applicability)[]
...values: (Node.Value | Node.Applicability | Node.Async)[]
): Node.MultiNode {
const m: Node.MultiNode = {
type: NodeType.MultiNode,
Expand All @@ -52,6 +58,25 @@ export class Builder {
return m;
}

/**
* Creates an async node
*
* @param id - the id of async node. It should be identical for each async node
*/
static asyncNode(id: string, flatten = true): Node.Async {
return {
id,
type: NodeType.Async,
flatten: flatten,
value: {
type: NodeType.Value,
value: {
id,
},
},
};
}

/**
* Adds a child node to a node
*
Expand Down
4 changes: 4 additions & 0 deletions core/player/src/view/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export declare namespace Node {
id: string;
/** The value representing the node */
value: Node;
/**
* Should the content streamed in be flattened during resolving
*/
flatten?: boolean;
}

export interface PluginOptions {
Expand Down
26 changes: 23 additions & 3 deletions core/player/src/view/resolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { DependencyModel, withParser } from "../../data";
import type { Logger } from "../../logger";
import type { Node } from "../parser";
import { NodeType } from "../parser";
import { caresAboutDataChanges, toNodeResolveOptions } from "./utils";
import {
caresAboutDataChanges,
toNodeResolveOptions,
unpackAndPush,
} from "./utils";
import type { Resolve } from "./types";
import { getNodeID } from "../parser/utils";

Expand Down Expand Up @@ -163,7 +167,6 @@ export class Resolver {
);
this.resolveCache = resolveCache;
this.hooks.afterUpdate.call(updated.value);

return updated.value;
}

Expand Down Expand Up @@ -408,7 +411,24 @@ export class Resolver {
);

if (mTree.value !== undefined && mTree.value !== null) {
childValue.push(mTree.value);
/**
* async nodes' parent is a multi-node
* When the node to resolve is an async node and the flatten flag is true
* Add the content streamed in to the childValue of parent multi-node
* Array.isArray(mTree.value.asset.values) is the case when the content is an async asset
*/
if (
mValue.type === NodeType.Async &&
mValue.flatten &&
mTree.value.asset &&
Array.isArray(mTree.value.asset.values)
) {
mTree.value.asset.values.forEach((v: any) => {
unpackAndPush(v, childValue);
});
} else {
childValue.push(mTree.value);
}
}

mTree.dependencies.forEach((bindingDep) =>
Expand Down
82 changes: 68 additions & 14 deletions docs/site/src/content/docs/plugins/multiplatform/async-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,59 @@ import PlatformTabs from "../../../../components/PlatformTabs.astro";
The AsyncNode Plugin is used to enable streaming additional content into a flow that has already been loaded and rendered.
A common use case for this plugin is conversational UI, as the users input more dialogue, new content must be streamed into Player in order to keep the UI up to date.

The pillar that makes this possible is the concept of an `AsyncNode`. An `AsyncNode` is any tree node with the property `async: true`, it represents a placeholder node that will be replaced by a concrete node in the future.
The pillar that makes this possible is the concept of an `AsyncNode`. An `AsyncNode` is any tree node with the property `async: true`, it represents a placeholder node that will be replaced by a concrete node in the future. `AsyncNode` is added to the AST tree during asset transform process(You can read more about what transform is [here](/assets/transforms)). In the example below, it shows the content and resolved result.

In the example below, node with the id "some-async-node" will not be rendered on first render, but will be replaced with a UI asset node at a later time:
#### Content authoring:

Value of `chat-message` is the asset to render
- JSON content
```json
{
"id": "flow-1",
"views": [
{
"id": 'action',
"actions": [
{
"id": "some-async-node",
"async": true,
"id": "chat-1",
"type": "chat-message",
"value": {
"id": "text-1",
"type": "text",
"value": "chat message",
},
},
```
- DSL
```typescript
<ChatMessage id="chat">
<ChatMessage.Value>
<Text>Hello World!</Text>
</ChatMessage.Value>
</ChatMessage>
```

#### Resolved

`chat-message` asset is wrapped into a wrapper container with children including the `Text` asset in value and an `AsyncNode` in the associated asset transform before resolving.
```json
{
"id": "collection-async-chat-1",
"type": "collection",
"values": [
{
"asset": {
"id": "1",
"type": "text",
"value": "chat message",
},
],
},
],
}
```
If there is no asset to display, there will be only one `AsyncNode` in resolved result and no content will be rendered.
```json
{
"id": "chat-1",
"type": "chat-message",
},
],
...
}
```


The `AsyncNodePlugin` exposes an `onAsyncNode` hook on all platforms. The `onAsyncNode` hook will be invoked with the current node when the plugin is available and an `AsyncNode` is detected during the resolve process. The node used to call the hook with could contain metadata according to content spec.

User should tap into the `onAsyncNode` hook to examine the node's metadata before making a decision on what to replace the async node with. The return could be a single asset node or an array of asset nodes, or the return could be even a null|undefined if the async node is no longer relevant.
Expand Down Expand Up @@ -218,3 +249,26 @@ AndroidPlayer(asyncNodePlugin)

</Fragment>
</PlatformTabs>


### AsyncTransform function
`AsyncTransform` is a helper function to create the transform for async asset. With the example below,
- id - `chat-message` asset id
- asset - value asset to pass in
- wrapperType - container asset type eg. `Collection`
- flatte - default value is true. Determine if the content streamed in should be flattened.

```json
{
"id": "chat",
"type": "chat-message",
"value": {
"id": "text",
"type": "text",
"value": "chat message",
},
},
```

### Flatten
Flatten is introduced to avoid nested structure caused by continuous streamning. eg. When a new `chat-message` asset is streamed in, in AST tree, instead of `[chat1, [chat2, asyncNode]]`, it's flattened into `[chat1, chat2, asyncNode]`
9 changes: 9 additions & 0 deletions docs/storybook/src/reference-assets/ChatMessage.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as ChatMessageStories from "./ChatMessage.stories";

# ChatMessage Asset

The `ChatMessage` asset is used as an example to demostrate the async streaming feature.

## Basic Use Case

The example below shows the basic usage of async node. Transform the `ChatMessage` asset and render the `text` asset with given value
15 changes: 15 additions & 0 deletions docs/storybook/src/reference-assets/ChatMessage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createDSLStory } from "@player-ui/storybook";
import { Meta } from "@storybook/react";

const meta: Meta = {
title: "Reference Assets/ChatMessage",
};

export default meta;

export const Basic = createDSLStory(
() =>
import(
"!!raw-loader!@player-ui/mocks/chat-message/chat-message-basic.tsx"
),
);
40 changes: 40 additions & 0 deletions ios/demo/Sources/MockFlows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,43 @@ static let inputAssetPendingTransaction: String = """
}
}
"""

static let chatMessageBasic: String = """
{
"id": "chat",
"views": [
{
"id": "1",
"type": "chat-message",
"value": {
"asset":{
"id": "text",
"type": "text",
"value": "chat message",
},
},
},
],
"navigation": {
"BEGIN": "FLOW_1",
"FLOW_1": {
"startState": "VIEW_1",
"VIEW_1": {
"state_type": "VIEW",
"ref": "1",
"transitions": {
"*": "END_Done"
}
},
"END_Done": {
"state_type": "END",
"outcome": "DONE"
}
}
}
}

"""

public static let assetSections: [FlowLoader.FlowSection] = [
(title: "action", flows: [
Expand Down Expand Up @@ -1429,6 +1466,9 @@ static let inputAssetPendingTransaction: String = """
(title: "text", flows: [
(name: "basic", flow: MockFlows.textBasic),
(name: "with link", flow: MockFlows.textWithLink)
]),
(title: "chat message", flows: [
(name: "basic", flow: MockFlows.chatMessageBasic),
])
]

Expand Down
4 changes: 4 additions & 0 deletions plugins/async-node/core/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ js_pipeline(
"//:node_modules/tapable-ts",
"//:node_modules/timm",
],
test_deps = [
":node_modules/@player-ui/reference-assets-plugin",
"//:vitest_config",
]
)
3 changes: 3 additions & 0 deletions plugins/async-node/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"main": "src/index.ts",
"peerDependencies": {
"@player-ui/player": "workspace:*"
},
"devDependencies": {
"@player-ui/reference-assets-plugin": "workspace:*"
}
}

Loading