@player-ui/async-node-plugin 0.13.0 → 0.14.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/AsyncNodePlugin.native.js +467 -279
  2. package/dist/AsyncNodePlugin.native.js.map +1 -1
  3. package/dist/cjs/index.cjs +217 -20
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/index.legacy-esm.js +217 -18
  6. package/dist/index.mjs +217 -18
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +2 -2
  9. package/src/__tests__/__snapshots__/transform.test.ts.snap +1 -0
  10. package/src/__tests__/createAsyncTransform.test.ts +405 -0
  11. package/src/__tests__/index.test.ts +94 -13
  12. package/src/__tests__/transform.bench.ts +177 -0
  13. package/src/createAsyncTransform.ts +101 -0
  14. package/src/index.ts +93 -13
  15. package/src/transform.ts +5 -2
  16. package/src/types.ts +1 -0
  17. package/src/utils/__tests__/extractNodeFromPath.test.ts +181 -0
  18. package/src/utils/__tests__/requiresAssetWrapper.test.ts +63 -0
  19. package/src/utils/__tests__/traverseAndReplace.test.ts +182 -0
  20. package/src/utils/__tests__/unwrapAsset.test.ts +65 -0
  21. package/src/utils/extractNodeFromPath.ts +56 -0
  22. package/src/utils/index.ts +4 -0
  23. package/src/utils/requiresAssetWrapper.ts +14 -0
  24. package/src/utils/traverseAndReplace.ts +34 -0
  25. package/src/utils/unwrapAsset.ts +16 -0
  26. package/types/createAsyncTransform.d.ts +24 -0
  27. package/types/index.d.ts +16 -1
  28. package/types/transform.d.ts +2 -1
  29. package/types/types.d.ts +1 -1
  30. package/types/utils/extractNodeFromPath.d.ts +4 -0
  31. package/types/utils/index.d.ts +5 -0
  32. package/types/utils/requiresAssetWrapper.d.ts +3 -0
  33. package/types/utils/traverseAndReplace.d.ts +4 -0
  34. package/types/utils/unwrapAsset.d.ts +3 -0
@@ -6,25 +6,23 @@ import {
6
6
  PlayerPlugin,
7
7
  AssetTransformCorePlugin,
8
8
  BeforeTransformFunction,
9
+ Flow,
9
10
  } from "@player-ui/player";
10
11
  import { Player, Parser } from "@player-ui/player";
11
12
  import { waitFor } from "@testing-library/react";
12
13
  import {
13
14
  AsyncNodePlugin,
14
15
  AsyncNodePluginPlugin,
15
- asyncTransform,
16
+ createAsyncTransform,
16
17
  } from "../index";
17
18
  import { CheckPathPlugin } from "@player-ui/check-path-plugin";
18
19
  import { Registry } from "@player-ui/partial-match-registry";
19
20
 
20
- const transform: BeforeTransformFunction = (asset) => {
21
- const newAsset = asset.children?.[0]?.value;
22
-
23
- if (!newAsset) {
24
- return asyncTransform(asset.value.id, "collection");
25
- }
26
- return asyncTransform(asset.value.id, "collection", newAsset);
27
- };
21
+ const transform: BeforeTransformFunction = createAsyncTransform({
22
+ transformAssetType: "chat-message",
23
+ wrapperAssetType: "collection",
24
+ getNestedAsset: (node) => node.children?.[0]?.value,
25
+ });
28
26
 
29
27
  const transformPlugin = new AssetTransformCorePlugin(
30
28
  new Registry([[{ type: "chat-message" }, { beforeResolve: transform }]]),
@@ -110,6 +108,31 @@ describe("view", () => {
110
108
  },
111
109
  };
112
110
 
111
+ const simpleAsyncContent: Flow = {
112
+ id: "test-flow",
113
+ views: [
114
+ {
115
+ type: "view",
116
+ id: "my-view",
117
+ values: {
118
+ async: true,
119
+ id: "async-values",
120
+ },
121
+ },
122
+ ],
123
+ navigation: {
124
+ BEGIN: "FLOW_1",
125
+ FLOW_1: {
126
+ startState: "VIEW_1",
127
+ VIEW_1: {
128
+ state_type: "VIEW",
129
+ ref: "my-view",
130
+ transitions: {},
131
+ },
132
+ },
133
+ },
134
+ };
135
+
113
136
  const asyncNodeTest = async (resolvedValue: any) => {
114
137
  const plugin = new AsyncNodePlugin({
115
138
  plugins: [new AsyncNodePluginPlugin()],
@@ -189,6 +212,64 @@ describe("view", () => {
189
212
  expect(view?.actions.length).toBe(1);
190
213
  };
191
214
 
215
+ test("should resolve async content children", async () => {
216
+ const plugin = new AsyncNodePlugin({
217
+ plugins: [new AsyncNodePluginPlugin()],
218
+ });
219
+
220
+ const asyncNodeHandler = vi.fn();
221
+ asyncNodeHandler.mockResolvedValue([
222
+ {
223
+ asset: {
224
+ id: "test-1",
225
+ type: "text",
226
+ value: "Test 1",
227
+ },
228
+ },
229
+ {
230
+ asset: {
231
+ id: "test-2",
232
+ type: "text",
233
+ value: "Test 2",
234
+ },
235
+ },
236
+ ]);
237
+
238
+ plugin.hooks.onAsyncNode.tap("test", asyncNodeHandler);
239
+ const player = new Player({ plugins: [plugin] });
240
+
241
+ player.start(simpleAsyncContent);
242
+
243
+ await waitFor(() => {
244
+ expect(asyncNodeHandler).toHaveBeenCalled();
245
+ const playerState = player.getState();
246
+ expect(playerState.status).toBe("in-progress");
247
+ expect(
248
+ (playerState as InProgressState).controllers.view.currentView
249
+ ?.lastUpdate,
250
+ ).toStrictEqual(
251
+ expect.objectContaining({
252
+ values: [
253
+ {
254
+ asset: {
255
+ id: "test-1",
256
+ type: "text",
257
+ value: "Test 1",
258
+ },
259
+ },
260
+ {
261
+ asset: {
262
+ id: "test-2",
263
+ type: "text",
264
+ value: "Test 2",
265
+ },
266
+ },
267
+ ],
268
+ }),
269
+ );
270
+ });
271
+ });
272
+
192
273
  test("should return current node view when the resolved node is null", async () => {
193
274
  await asyncNodeTest(null);
194
275
  });
@@ -852,7 +933,7 @@ describe("view", () => {
852
933
 
853
934
  let deferredResolve: ((value: any) => void) | undefined;
854
935
 
855
- plugin.hooks.onAsyncNode.tap("test", async (node) => {
936
+ plugin.hooks.onAsyncNode.tap("test", async () => {
856
937
  return new Promise((resolve) => {
857
938
  deferredResolve = resolve;
858
939
  });
@@ -868,7 +949,7 @@ describe("view", () => {
868
949
 
869
950
  player.hooks.viewController.tap("async-node-test", (vc) => {
870
951
  vc.hooks.view.tap("async-node-test", (view) => {
871
- view.hooks.onUpdate.tap("async-node-test", (update) => {
952
+ view.hooks.onUpdate.tap("async-node-test", () => {
872
953
  updateNumber++;
873
954
  });
874
955
  });
@@ -913,8 +994,8 @@ describe("view", () => {
913
994
  view = (player.getState() as InProgressState).controllers.view.currentView
914
995
  ?.lastUpdate;
915
996
 
916
- expect(view?.values[1][0].asset.type).toBe("text");
917
- expect(view?.values[1][1].asset.type).toBe("text");
997
+ expect(view?.values[1].asset.type).toBe("text");
998
+ expect(view?.values[2].asset.type).toBe("text");
918
999
  });
919
1000
 
920
1001
  test("chat-message asset - replaces async nodes with provided node", async () => {
@@ -0,0 +1,177 @@
1
+ import { bench, BenchOptions, describe } from "vitest";
2
+ import {
3
+ AsyncNodePlugin,
4
+ AsyncNodePluginPlugin,
5
+ createAsyncTransform,
6
+ } from "..";
7
+ import {
8
+ Asset,
9
+ AssetTransformCorePlugin,
10
+ BeforeTransformFunction,
11
+ Flow,
12
+ Player,
13
+ PlayerPlugin,
14
+ } from "@player-ui/player";
15
+ import { Registry } from "@player-ui/partial-match-registry";
16
+
17
+ export const transform: BeforeTransformFunction<Asset<"chat-message">> =
18
+ createAsyncTransform({
19
+ transformAssetType: "chat-message",
20
+ wrapperAssetType: "collection",
21
+ getNestedAsset: (node) => node.children?.[0]?.value,
22
+ });
23
+
24
+ const asyncTransformBenchFlow: Flow = {
25
+ id: "test-flow",
26
+ views: [
27
+ {
28
+ id: "my-view",
29
+ type: "view",
30
+ values: [
31
+ {
32
+ asset: {
33
+ id: "chat",
34
+ type: "chat-message",
35
+ value: {
36
+ asset: {
37
+ type: "text",
38
+ id: "original-text",
39
+ value: "TEST",
40
+ },
41
+ },
42
+ },
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ navigation: {
48
+ BEGIN: "FLOW_1",
49
+ FLOW_1: {
50
+ startState: "VIEW_1",
51
+ VIEW_1: {
52
+ state_type: "VIEW",
53
+ ref: "my-view",
54
+ transitions: {
55
+ "*": "END_DONE",
56
+ },
57
+ },
58
+ END_DONE: {
59
+ state_type: "END",
60
+ outcome: "done",
61
+ },
62
+ },
63
+ },
64
+ };
65
+
66
+ const transformPlugin = new AssetTransformCorePlugin(
67
+ new Registry([[{ type: "chat-message" }, { beforeResolve: transform }]]),
68
+ );
69
+
70
+ class TestAsyncPlugin implements PlayerPlugin {
71
+ name = "test-async";
72
+ apply(player: Player) {
73
+ player.hooks.view.tap("test-async", (view) => {
74
+ transformPlugin.apply(view);
75
+ });
76
+ }
77
+ }
78
+
79
+ describe("async transform benchmarks", () => {
80
+ const asyncNodes = [1, 5, 10, 50, 100];
81
+
82
+ asyncNodes.forEach((nodeCount) => {
83
+ // Promise for when player reaches a completed state.
84
+ let playerCompletePromise: Promise<unknown>;
85
+ // Function to resolve the async node. Resolves the promise for the `onAsyncNode` hook.
86
+ let resolveAsyncNode: () => void;
87
+
88
+ // Setup function for spinning up player and setting up the above promise and function.
89
+ // Using a setup function also takes all the overhead of the setup itself out of the perf benchmark.
90
+ const setupPlayer = () => {
91
+ const asyncNodePlugin = new AsyncNodePlugin({
92
+ plugins: [new AsyncNodePluginPlugin()],
93
+ });
94
+
95
+ let completeSetup: (value?: unknown) => void = () => {};
96
+ const setupPromise = new Promise((res) => {
97
+ completeSetup = res;
98
+ });
99
+
100
+ let lastCreatedNodeIndex = -1;
101
+ asyncNodePlugin.hooks.onAsyncNode.tap("bench", () => {
102
+ return new Promise((resolve) => {
103
+ const nodeNumber = lastCreatedNodeIndex + 1;
104
+ // Setup the resolve function to add a text asset and another async node.
105
+ resolveAsyncNode = () => {
106
+ lastCreatedNodeIndex = nodeNumber;
107
+ resolve([
108
+ {
109
+ asset: {
110
+ id: `chat-${nodeNumber}`,
111
+ type: "chat-message",
112
+ value: {
113
+ asset: {
114
+ type: "text",
115
+ id: `chat-label-${nodeNumber}`,
116
+ value: "Test",
117
+ },
118
+ },
119
+ },
120
+ },
121
+ ]);
122
+ };
123
+
124
+ // If this is not the last node to be added for this test, resolve it immediately, otherwise resolve the setup promise.
125
+ if (nodeNumber + 1 < nodeCount) {
126
+ resolveAsyncNode();
127
+ } else {
128
+ completeSetup();
129
+ }
130
+ });
131
+ });
132
+
133
+ const player = new Player({
134
+ plugins: [asyncNodePlugin, new TestAsyncPlugin()],
135
+ });
136
+
137
+ player.hooks.view.tap("bench", (fc) => {
138
+ fc.hooks.onUpdate.tap("bench", () => {
139
+ // Since the created index should go up to `nodeCount - 1` we wait for that to be true before transitioning player to its end state.
140
+ if (lastCreatedNodeIndex < nodeCount - 1) {
141
+ return;
142
+ }
143
+
144
+ const state = player.getState();
145
+
146
+ if (state.status !== "in-progress") {
147
+ throw new Error("benchmark failed");
148
+ }
149
+
150
+ state.controllers.flow.transition("END");
151
+ });
152
+ });
153
+
154
+ playerCompletePromise = player.start(asyncTransformBenchFlow);
155
+
156
+ return setupPromise;
157
+ };
158
+
159
+ // The bench setup function. This gets called once before all iterations of the test are run
160
+ const setup: BenchOptions["setup"] = (task) => {
161
+ // Add setup to the before each and wait on it to finish before starting a test run.
162
+ task.opts.beforeEach = async () => {
163
+ await setupPlayer();
164
+ };
165
+ };
166
+
167
+ bench(
168
+ `Resolve Async Node ${nodeCount} times`,
169
+ async () => {
170
+ // The test just resolves the last node and waits for player to complete.
171
+ resolveAsyncNode();
172
+ await playerCompletePromise;
173
+ },
174
+ { iterations: 100, throws: true, setup },
175
+ );
176
+ });
177
+ });
@@ -0,0 +1,101 @@
1
+ import {
2
+ BeforeTransformFunction,
3
+ Builder,
4
+ Node,
5
+ NodeType,
6
+ } from "@player-ui/player";
7
+ import {
8
+ extractNodeFromPath,
9
+ requiresAssetWrapper,
10
+ traverseAndReplace,
11
+ unwrapAsset,
12
+ } from "./utils";
13
+
14
+ export type AsyncTransformOptions = {
15
+ /** Whether or not to flatten the results into its container. Defaults to true */
16
+ flatten?: boolean;
17
+ /** The path to the array within the `wrapperAssetType` that will contain the async content. Defaults to ["values"] */
18
+ path?: string[];
19
+ /** The asset type that the transform is matching against. */
20
+ transformAssetType: string;
21
+ /** The asset type that will contain the async content. */
22
+ wrapperAssetType: string;
23
+ /** Function to get any nested asset that will need to be extracted and kept when creating the wrapper asset. */
24
+ getNestedAsset?: (node: Node.ViewOrAsset) => Node.Node | undefined;
25
+ /** Function to get the id for the async node being generated. Defaults to creating an id with the format of async-<ASSET.ID> */
26
+ getAsyncNodeId?: (node: Node.ViewOrAsset) => string;
27
+ };
28
+
29
+ const defaultGetNodeId = (node: Node.ViewOrAsset): string => {
30
+ return `async-${node.value.id}`;
31
+ };
32
+
33
+ /** Creates a BeforeTransformFunction that turns the given asset into a wrapper asset with an async node in it.
34
+ * By setting {@link AsyncTransformOptions.flatten} to true, you can chain multiple of the same asset type to create a flow of async content that
35
+ * exists within a single collection.
36
+ *
37
+ * @param options - Options for managing the transform
38
+ * @returns The {@link BeforeTransformFunction} that can be used for your asset.
39
+ */
40
+ export const createAsyncTransform = (
41
+ options: AsyncTransformOptions,
42
+ ): BeforeTransformFunction => {
43
+ const {
44
+ transformAssetType,
45
+ wrapperAssetType,
46
+ getNestedAsset,
47
+ getAsyncNodeId = defaultGetNodeId,
48
+ path = ["values"],
49
+ flatten = true,
50
+ } = options;
51
+
52
+ const replaceNode = (node: Node.Node): Node.Node => {
53
+ const unwrapped = unwrapAsset(node);
54
+
55
+ if (
56
+ unwrapped.type !== NodeType.Asset ||
57
+ unwrapped.value.type !== transformAssetType
58
+ ) {
59
+ return node;
60
+ }
61
+
62
+ const transformed = asyncTransform(unwrapped);
63
+ return extractNodeFromPath(transformed, path) ?? node;
64
+ };
65
+
66
+ const replacer = (node: Node.Node) => traverseAndReplace(node, replaceNode);
67
+
68
+ const asyncTransform = (node: Node.ViewOrAsset) => {
69
+ const id = getAsyncNodeId(node);
70
+ const asset = getNestedAsset?.(node);
71
+
72
+ // If flattening is disabled, don't need to extract the multi-node when async node is resolved.
73
+ const replaceFunction = flatten ? replacer : undefined;
74
+ const asyncNode = Builder.asyncNode(id, flatten, replaceFunction);
75
+
76
+ let multiNode: Node.MultiNode | undefined;
77
+ if (asset) {
78
+ if (requiresAssetWrapper(asset)) {
79
+ const assetWrappedNode = Builder.assetWrapper(asset);
80
+ multiNode = Builder.multiNode(assetWrappedNode, asyncNode);
81
+ } else if (asset.type === NodeType.MultiNode) {
82
+ multiNode = Builder.multiNode(...(asset.values as any[]), asyncNode);
83
+ } else {
84
+ multiNode = Builder.multiNode(asset as any, asyncNode);
85
+ }
86
+ } else {
87
+ multiNode = Builder.multiNode(asyncNode);
88
+ }
89
+
90
+ const wrapperAsset: Node.ViewOrAsset = Builder.asset({
91
+ id: wrapperAssetType + "-" + id,
92
+ type: wrapperAssetType,
93
+ });
94
+
95
+ Builder.addChild(wrapperAsset, path, multiNode);
96
+
97
+ return wrapperAsset;
98
+ };
99
+
100
+ return asyncTransform;
101
+ };
package/src/index.ts CHANGED
@@ -13,10 +13,10 @@ import type {
13
13
  } from "@player-ui/player";
14
14
  import { AsyncParallelBailHook, SyncBailHook } from "tapable-ts";
15
15
  import queueMicrotask from "queue-microtask";
16
- import { omit } from "timm";
17
16
 
18
17
  export * from "./types";
19
18
  export * from "./transform";
19
+ export * from "./createAsyncTransform";
20
20
 
21
21
  /** Object type for storing data related to a single `apply` of the `AsyncNodePluginPlugin`
22
22
  * This object should be setup once per ViewInstance to keep any cached info just for that view to avoid conflicts of shared async node ids across different view states.
@@ -46,8 +46,14 @@ export type AsyncHandler = (
46
46
  callback?: (result: any) => void,
47
47
  ) => Promise<any>;
48
48
 
49
+ export type AsyncContent = {
50
+ async: true;
51
+ flatten?: boolean;
52
+ [key: string]: unknown;
53
+ };
54
+
49
55
  /** Hook declaration for the AsyncNodePlugin */
50
- type AsyncNodeHooks = {
56
+ export type AsyncNodeHooks = {
51
57
  /** Async hook to get content for an async node */
52
58
  onAsyncNode: AsyncParallelBailHook<[Node.Async, (result: any) => void], any>;
53
59
  /** Sync hook to manage errors coming from the onAsyncNode hook. Return a fallback node or null to render a fallback. The first argument of passed in the call is the error thrown. */
@@ -131,9 +137,13 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
131
137
  result: any,
132
138
  options: Resolve.NodeResolveOptions,
133
139
  ) {
134
- const parsedNode =
140
+ let parsedNode =
135
141
  options.parseNode && result ? options.parseNode(result) : undefined;
136
142
 
143
+ if (parsedNode && node.onValueReceived) {
144
+ parsedNode = node.onValueReceived(parsedNode);
145
+ }
146
+
137
147
  this.handleAsyncUpdate(node, context, parsedNode);
138
148
  }
139
149
 
@@ -154,10 +164,20 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
154
164
  const { nodeResolveCache, view } = context;
155
165
  if (nodeResolveCache.get(node.id) !== newNode) {
156
166
  nodeResolveCache.set(node.id, newNode ? newNode : node);
157
- view.updateAsync();
167
+ view.updateAsync(node.id);
158
168
  }
159
169
  }
160
170
 
171
+ private hasValidMapping(
172
+ node: Node.Async,
173
+ context: AsyncPluginContext,
174
+ ): boolean {
175
+ const { nodeResolveCache } = context;
176
+ return (
177
+ nodeResolveCache.has(node.id) && nodeResolveCache.get(node.id) !== node
178
+ );
179
+ }
180
+
161
181
  /**
162
182
  * Handles the asynchronous API integration for resolving nodes.
163
183
  * This method sets up a hook on the resolver's `beforeResolve` event to process async nodes.
@@ -167,12 +187,12 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
167
187
  applyResolver(resolver: Resolver, context: AsyncPluginContext): void {
168
188
  resolver.hooks.beforeResolve.tap(this.name, (node, options) => {
169
189
  if (!this.isAsync(node)) {
170
- return node;
190
+ return node === null ? node : this.resolveAsyncChildren(node, context);
171
191
  }
172
192
 
173
193
  const resolvedNode = context.nodeResolveCache.get(node.id);
174
194
  if (resolvedNode !== undefined) {
175
- return resolvedNode;
195
+ return this.resolveAsyncChildren(resolvedNode, context);
176
196
  }
177
197
 
178
198
  if (context.inProgressNodes.has(node.id)) {
@@ -189,6 +209,63 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
189
209
  });
190
210
  }
191
211
 
212
+ /**
213
+ * Replaces child async nodes with their resolved content and flattens when necessary. Resolving the children directly helps manage the `parent` reference without needing as much work within the resolver itself.
214
+ * Handles async node chains as well to make sure all applicable nodes can get flattened.
215
+ * @param node - The node whose children need to be resolved.
216
+ * @param context - the async plugin context needed to reach into the cache
217
+ * @returns The same node but with async node children mapped to their resolved AST.
218
+ */
219
+ private resolveAsyncChildren(
220
+ node: Node.Node,
221
+ context: AsyncPluginContext,
222
+ ): Node.Node {
223
+ const asyncNodesResolved: string[] = node.asyncNodesResolved ?? [];
224
+ node.asyncNodesResolved = asyncNodesResolved;
225
+ if (node.type === NodeType.MultiNode) {
226
+ // Using a while loop lets us catch when async nodes produce more async nodes that need to be flattened further
227
+ let index = 0;
228
+ while (index < node.values.length) {
229
+ const childNode = node.values[index];
230
+ if (
231
+ childNode?.type !== NodeType.Async ||
232
+ !this.hasValidMapping(childNode, context)
233
+ ) {
234
+ index++;
235
+ continue;
236
+ }
237
+
238
+ const mappedNode = context.nodeResolveCache.get(childNode.id);
239
+ asyncNodesResolved.push(childNode.id);
240
+ if (mappedNode.type === NodeType.MultiNode && childNode.flatten) {
241
+ mappedNode.values.forEach((v: Node.Node) => (v.parent = node));
242
+ node.values = [
243
+ ...node.values.slice(0, index),
244
+ ...mappedNode.values,
245
+ ...node.values.slice(index + 1),
246
+ ];
247
+ } else {
248
+ node.values[index] = mappedNode;
249
+ mappedNode.parent = node;
250
+ }
251
+ }
252
+ } else if ("children" in node) {
253
+ node.children?.forEach((c) => {
254
+ // Similar to above, using a while loop lets us handle when async nodes produce more async nodes.
255
+ while (
256
+ c.value.type === NodeType.Async &&
257
+ this.hasValidMapping(c.value, context)
258
+ ) {
259
+ asyncNodesResolved.push(c.value.id);
260
+ c.value = context.nodeResolveCache.get(c.value.id);
261
+ c.value.parent = node;
262
+ }
263
+ });
264
+ }
265
+
266
+ return node;
267
+ }
268
+
192
269
  private async runAsyncNode(
193
270
  node: Node.Async,
194
271
  context: AsyncPluginContext,
@@ -234,8 +311,12 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
234
311
  return node?.type === NodeType.Async;
235
312
  }
236
313
 
237
- private isDeterminedAsync(obj: any) {
238
- return obj && Object.prototype.hasOwnProperty.call(obj, "async");
314
+ private isDeterminedAsync(obj: unknown): obj is AsyncContent {
315
+ return (
316
+ typeof obj === "object" &&
317
+ obj !== null &&
318
+ Object.prototype.hasOwnProperty.call(obj, "async")
319
+ );
239
320
  }
240
321
 
241
322
  applyParser(parser: Parser): void {
@@ -248,11 +329,9 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
248
329
  childOptions?: ParseObjectChildOptions,
249
330
  ) => {
250
331
  if (this.isDeterminedAsync(obj)) {
251
- const parsedAsync = parser.parseObject(
252
- omit(obj, "async"),
253
- nodeType,
254
- options,
255
- );
332
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
333
+ const { async, flatten, ...rest } = obj;
334
+ const parsedAsync = parser.parseObject(rest, nodeType, options);
256
335
  const parsedNodeId = getNodeID(parsedAsync);
257
336
 
258
337
  if (parsedAsync === null || !parsedNodeId) {
@@ -264,6 +343,7 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
264
343
  id: parsedNodeId,
265
344
  type: NodeType.Async,
266
345
  value: parsedAsync,
346
+ flatten,
267
347
  },
268
348
  obj,
269
349
  );
package/src/transform.ts CHANGED
@@ -2,11 +2,12 @@ import { Builder } from "@player-ui/player";
2
2
  import type { AsyncTransformFunc } from "./types";
3
3
 
4
4
  /**
5
+ * @deprecated Use {@link createAsyncTransform} to create your before transform function.
5
6
  * Util function to generate transform function for async asset
6
7
  * @param asset - async asset to apply beforeResolve transform
7
- * @param transformedAssetType: transformed asset type for rendering
8
8
  * @param wrapperAssetType: container asset type
9
9
  * @param flatten: flatten the streamed in content
10
+ * @param path: property path to add the multinode containing the next async node to
10
11
  * @returns - wrapper asset with children of transformed asset and async node
11
12
  */
12
13
 
@@ -15,10 +16,12 @@ export const asyncTransform: AsyncTransformFunc = (
15
16
  wrapperAssetType,
16
17
  asset,
17
18
  flatten,
19
+ path = ["values"],
18
20
  ) => {
19
21
  const id = "async-" + assetId;
20
22
 
21
23
  const asyncNode = Builder.asyncNode(id, flatten);
24
+
22
25
  let multiNode;
23
26
  let assetNode;
24
27
 
@@ -34,7 +37,7 @@ export const asyncTransform: AsyncTransformFunc = (
34
37
  type: wrapperAssetType,
35
38
  });
36
39
 
37
- Builder.addChild(wrapperAsset, ["values"], multiNode);
40
+ Builder.addChild(wrapperAsset, path, multiNode);
38
41
 
39
42
  return wrapperAsset;
40
43
  };
package/src/types.ts CHANGED
@@ -10,4 +10,5 @@ export type AsyncTransformFunc = (
10
10
  wrapperAssetType: string,
11
11
  asset?: Node.Node,
12
12
  flatten?: boolean,
13
+ path?: string[],
13
14
  ) => Node.Asset;