@player-ui/react 0.16.0--canary.866.36538 → 1.0.0-next.0
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.
- package/dist/cjs/index.cjs +112 -24
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +113 -19
- package/dist/index.mjs +113 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/player.test.tsx +263 -2
- package/src/asset/AssetRenderError.ts +20 -2
- package/src/asset/__tests__/index.test.tsx +3 -3
- package/src/asset/index.tsx +2 -0
- package/src/manager/__tests__/__snapshots__/managed-player.test.tsx.snap +249 -0
- package/src/manager/__tests__/managed-player.test.tsx +20 -3
- package/src/manager/managed-player.tsx +3 -5
- package/src/manager/types.ts +10 -10
- package/src/player.tsx +154 -26
- package/types/asset/AssetRenderError.d.ts +8 -2
- package/types/manager/types.d.ts +10 -8
- package/types/player.d.ts +14 -8
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ManagedPlayer with React 17 > handles new manager 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"data": {},
|
|
6
|
+
"id": "generated-flow",
|
|
7
|
+
"navigation": {
|
|
8
|
+
"BEGIN": "FLOW_1",
|
|
9
|
+
"FLOW_1": {
|
|
10
|
+
"END_Done": {
|
|
11
|
+
"outcome": "done",
|
|
12
|
+
"state_type": "END",
|
|
13
|
+
},
|
|
14
|
+
"VIEW_1": {
|
|
15
|
+
"ref": "flow-1-1",
|
|
16
|
+
"state_type": "VIEW",
|
|
17
|
+
"transitions": {
|
|
18
|
+
"*": "END_Done",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"startState": "VIEW_1",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"views": [
|
|
25
|
+
{
|
|
26
|
+
"id": "flow-1-1",
|
|
27
|
+
"type": "collection",
|
|
28
|
+
"values": [
|
|
29
|
+
{
|
|
30
|
+
"asset": {
|
|
31
|
+
"id": "action",
|
|
32
|
+
"label": "Continue",
|
|
33
|
+
"type": "action",
|
|
34
|
+
"value": "Next",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
exports[`ManagedPlayer with React 17 > handles new manager 2`] = `
|
|
44
|
+
{
|
|
45
|
+
"data": {},
|
|
46
|
+
"id": "generated-flow",
|
|
47
|
+
"navigation": {
|
|
48
|
+
"BEGIN": "FLOW_1",
|
|
49
|
+
"FLOW_1": {
|
|
50
|
+
"END_Done": {
|
|
51
|
+
"outcome": "done",
|
|
52
|
+
"state_type": "END",
|
|
53
|
+
},
|
|
54
|
+
"VIEW_1": {
|
|
55
|
+
"ref": "flow-1-2",
|
|
56
|
+
"state_type": "VIEW",
|
|
57
|
+
"transitions": {
|
|
58
|
+
"*": "END_Done",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
"startState": "VIEW_1",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
"views": [
|
|
65
|
+
{
|
|
66
|
+
"id": "flow-1-2",
|
|
67
|
+
"type": "collection",
|
|
68
|
+
"values": [
|
|
69
|
+
{
|
|
70
|
+
"asset": {
|
|
71
|
+
"id": "action",
|
|
72
|
+
"label": "Continue",
|
|
73
|
+
"type": "action",
|
|
74
|
+
"value": "Next",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
exports[`ManagedPlayer with React 17 > handles terminating with data 1`] = `
|
|
84
|
+
{
|
|
85
|
+
"data": {
|
|
86
|
+
"returns": {
|
|
87
|
+
"id": "123",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
"id": "generated-flow",
|
|
91
|
+
"navigation": {
|
|
92
|
+
"BEGIN": "FLOW_1",
|
|
93
|
+
"FLOW_1": {
|
|
94
|
+
"END_Done": {
|
|
95
|
+
"outcome": "done",
|
|
96
|
+
"state_type": "END",
|
|
97
|
+
},
|
|
98
|
+
"VIEW_1": {
|
|
99
|
+
"ref": "flow-1",
|
|
100
|
+
"state_type": "VIEW",
|
|
101
|
+
"transitions": {
|
|
102
|
+
"*": "END_Done",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"startState": "VIEW_1",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
"views": [
|
|
109
|
+
{
|
|
110
|
+
"id": "flow-1",
|
|
111
|
+
"type": "collection",
|
|
112
|
+
"values": [
|
|
113
|
+
{
|
|
114
|
+
"asset": {
|
|
115
|
+
"id": "action",
|
|
116
|
+
"label": "Continue",
|
|
117
|
+
"type": "action",
|
|
118
|
+
"value": "Next",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
exports[`ManagedPlayer with React 18 > handles new manager 1`] = `
|
|
128
|
+
{
|
|
129
|
+
"data": {},
|
|
130
|
+
"id": "generated-flow",
|
|
131
|
+
"navigation": {
|
|
132
|
+
"BEGIN": "FLOW_1",
|
|
133
|
+
"FLOW_1": {
|
|
134
|
+
"END_Done": {
|
|
135
|
+
"outcome": "done",
|
|
136
|
+
"state_type": "END",
|
|
137
|
+
},
|
|
138
|
+
"VIEW_1": {
|
|
139
|
+
"ref": "flow-1-1",
|
|
140
|
+
"state_type": "VIEW",
|
|
141
|
+
"transitions": {
|
|
142
|
+
"*": "END_Done",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"startState": "VIEW_1",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"views": [
|
|
149
|
+
{
|
|
150
|
+
"id": "flow-1-1",
|
|
151
|
+
"type": "collection",
|
|
152
|
+
"values": [
|
|
153
|
+
{
|
|
154
|
+
"asset": {
|
|
155
|
+
"id": "action",
|
|
156
|
+
"label": "Continue",
|
|
157
|
+
"type": "action",
|
|
158
|
+
"value": "Next",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
exports[`ManagedPlayer with React 18 > handles new manager 2`] = `
|
|
168
|
+
{
|
|
169
|
+
"data": {},
|
|
170
|
+
"id": "generated-flow",
|
|
171
|
+
"navigation": {
|
|
172
|
+
"BEGIN": "FLOW_1",
|
|
173
|
+
"FLOW_1": {
|
|
174
|
+
"END_Done": {
|
|
175
|
+
"outcome": "done",
|
|
176
|
+
"state_type": "END",
|
|
177
|
+
},
|
|
178
|
+
"VIEW_1": {
|
|
179
|
+
"ref": "flow-1-2",
|
|
180
|
+
"state_type": "VIEW",
|
|
181
|
+
"transitions": {
|
|
182
|
+
"*": "END_Done",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
"startState": "VIEW_1",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
"views": [
|
|
189
|
+
{
|
|
190
|
+
"id": "flow-1-2",
|
|
191
|
+
"type": "collection",
|
|
192
|
+
"values": [
|
|
193
|
+
{
|
|
194
|
+
"asset": {
|
|
195
|
+
"id": "action",
|
|
196
|
+
"label": "Continue",
|
|
197
|
+
"type": "action",
|
|
198
|
+
"value": "Next",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
exports[`ManagedPlayer with React 18 > handles terminating with data 1`] = `
|
|
208
|
+
{
|
|
209
|
+
"data": {
|
|
210
|
+
"returns": {
|
|
211
|
+
"id": "123",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
"id": "generated-flow",
|
|
215
|
+
"navigation": {
|
|
216
|
+
"BEGIN": "FLOW_1",
|
|
217
|
+
"FLOW_1": {
|
|
218
|
+
"END_Done": {
|
|
219
|
+
"outcome": "done",
|
|
220
|
+
"state_type": "END",
|
|
221
|
+
},
|
|
222
|
+
"VIEW_1": {
|
|
223
|
+
"ref": "flow-1",
|
|
224
|
+
"state_type": "VIEW",
|
|
225
|
+
"transitions": {
|
|
226
|
+
"*": "END_Done",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
"startState": "VIEW_1",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
"views": [
|
|
233
|
+
{
|
|
234
|
+
"id": "flow-1",
|
|
235
|
+
"type": "collection",
|
|
236
|
+
"values": [
|
|
237
|
+
{
|
|
238
|
+
"asset": {
|
|
239
|
+
"id": "action",
|
|
240
|
+
"label": "Continue",
|
|
241
|
+
"type": "action",
|
|
242
|
+
"value": "Next",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { ManagedPlayer } from "../managed-player";
|
|
11
11
|
import type { FlowManager, FallbackProps } from "../types";
|
|
12
12
|
import { SimpleAssetPlugin } from "../../__tests__/helpers/simple-asset-plugin";
|
|
13
|
+
import { InProgressState } from "@player-ui/player";
|
|
13
14
|
|
|
14
15
|
vitest.mock("@player-ui/metrics-plugin", async () => {
|
|
15
16
|
const actual: object = await vitest.importActual("@player-ui/metrics-plugin");
|
|
@@ -452,7 +453,11 @@ describe.each([
|
|
|
452
453
|
|
|
453
454
|
await screen.findByTestId("flow-1");
|
|
454
455
|
result.unmount();
|
|
455
|
-
|
|
456
|
+
|
|
457
|
+
const terminateArgument: InProgressState =
|
|
458
|
+
manager.terminate?.mock.calls[0][0];
|
|
459
|
+
expect(terminateArgument.flow).toMatchSnapshot();
|
|
460
|
+
expect(terminateArgument.status).toBe("in-progress");
|
|
456
461
|
});
|
|
457
462
|
|
|
458
463
|
test("handles new manager", async () => {
|
|
@@ -557,7 +562,13 @@ describe.each([
|
|
|
557
562
|
let newManagerBtn = await screen.findByTestId("newManager");
|
|
558
563
|
await user.click(newManagerBtn);
|
|
559
564
|
|
|
560
|
-
|
|
565
|
+
// terminate should receive an InProgressState
|
|
566
|
+
expect(previousManager.current.terminate).toBeCalled();
|
|
567
|
+
const terminateArgument1: InProgressState =
|
|
568
|
+
previousManager.current.terminate?.mock.calls[0][0];
|
|
569
|
+
expect(terminateArgument1.flow).toMatchSnapshot();
|
|
570
|
+
expect(terminateArgument1.status).toBe("in-progress");
|
|
571
|
+
|
|
561
572
|
expect(previousManager.current.next).toBeCalledTimes(1);
|
|
562
573
|
expect(manager.next).toBeCalledTimes(1);
|
|
563
574
|
await screen.findByTestId("flow-1-2");
|
|
@@ -566,7 +577,13 @@ describe.each([
|
|
|
566
577
|
await user.click(newManagerBtn);
|
|
567
578
|
|
|
568
579
|
const prevMan = previousManager.current;
|
|
569
|
-
|
|
580
|
+
// terminate should receive an InProgressState
|
|
581
|
+
expect(prevMan.terminate).toBeCalled();
|
|
582
|
+
const terminateArgument2: InProgressState =
|
|
583
|
+
prevMan.terminate?.mock.calls[0][0];
|
|
584
|
+
expect(terminateArgument2.flow).toMatchSnapshot();
|
|
585
|
+
expect(terminateArgument2.status).toBe("in-progress");
|
|
586
|
+
|
|
570
587
|
expect(prevMan.next).toBeCalledTimes(1);
|
|
571
588
|
expect(manager.next).toBeCalledTimes(1);
|
|
572
589
|
await screen.findByTestId("flow-1-3");
|
|
@@ -252,9 +252,7 @@ export const usePersistentStateMachine = (options: {
|
|
|
252
252
|
oldManagedState.state?.value === "running" &&
|
|
253
253
|
playerState?.status === "in-progress"
|
|
254
254
|
) {
|
|
255
|
-
previousManager.current.terminate?.(
|
|
256
|
-
playerState.controllers.data.serialize(),
|
|
257
|
-
);
|
|
255
|
+
previousManager.current.terminate?.(playerState);
|
|
258
256
|
}
|
|
259
257
|
}
|
|
260
258
|
|
|
@@ -341,7 +339,7 @@ export const ManagedPlayer = (
|
|
|
341
339
|
} else if (state?.value === "error") {
|
|
342
340
|
props.onError?.(state?.context.error);
|
|
343
341
|
} else if (state?.value === "running") {
|
|
344
|
-
props.onStartedFlow?.();
|
|
342
|
+
props.onStartedFlow?.(state.context.flow);
|
|
345
343
|
}
|
|
346
344
|
}
|
|
347
345
|
|
|
@@ -352,7 +350,7 @@ export const ManagedPlayer = (
|
|
|
352
350
|
const playerState = state?.context.reactPlayer.player.getState();
|
|
353
351
|
|
|
354
352
|
if (state?.value === "running" && playerState?.status === "in-progress") {
|
|
355
|
-
props.manager.terminate?.(playerState
|
|
353
|
+
props.manager.terminate?.(playerState);
|
|
356
354
|
}
|
|
357
355
|
};
|
|
358
356
|
}, [props.manager, state?.context.reactPlayer.player, state?.value]);
|
package/src/manager/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
-
import type { CompletedState, Flow,
|
|
2
|
+
import type { CompletedState, Flow, InProgressState } from "@player-ui/player";
|
|
3
3
|
import type { ReactPlayer, ReactPlayerOptions } from "../player";
|
|
4
4
|
|
|
5
5
|
export interface FinalState {
|
|
@@ -7,14 +7,16 @@ export interface FinalState {
|
|
|
7
7
|
done: true;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export interface NextState
|
|
10
|
+
export interface NextState {
|
|
11
11
|
/** Optional mark the iteration as _not_ completed */
|
|
12
|
-
done
|
|
12
|
+
done: false;
|
|
13
13
|
|
|
14
14
|
/** The next value in the iteration */
|
|
15
|
-
value:
|
|
15
|
+
value: Flow;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** A JS Iterator that returns a new flow or completion marker
|
|
19
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator */
|
|
18
20
|
export interface FlowManager {
|
|
19
21
|
/**
|
|
20
22
|
* An iterator implementation that takes the result of the previous flow and returns a new one or completion marker.
|
|
@@ -23,15 +25,13 @@ export interface FlowManager {
|
|
|
23
25
|
*
|
|
24
26
|
* @param previousValue - The result of the previous flow.
|
|
25
27
|
*/
|
|
26
|
-
next: (
|
|
27
|
-
previousValue?: CompletedState,
|
|
28
|
-
) => Promise<FinalState | NextState<Flow>>;
|
|
28
|
+
next: (result?: CompletedState) => Promise<FinalState | NextState>;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Called when the flow is ended early (the react tree is torn down)
|
|
32
32
|
* Allows clients the opportunity to save-data before destroying the tree
|
|
33
33
|
*/
|
|
34
|
-
terminate?: (
|
|
34
|
+
terminate?: (state?: InProgressState) => void;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export interface FallbackProps {
|
|
@@ -50,7 +50,7 @@ export interface ManagedPlayerProps extends ReactPlayerOptions {
|
|
|
50
50
|
manager: FlowManager;
|
|
51
51
|
|
|
52
52
|
/** A callback when a flow is started */
|
|
53
|
-
onStartedFlow?: () => void;
|
|
53
|
+
onStartedFlow?: (flow: Flow) => void;
|
|
54
54
|
|
|
55
55
|
/** A callback when the entire async iteration is completed */
|
|
56
56
|
onComplete?: (finalState?: CompletedState) => void;
|
|
@@ -91,7 +91,7 @@ export type ManagedPlayerState =
|
|
|
91
91
|
prevResult?: CompletedState;
|
|
92
92
|
|
|
93
93
|
/** A promise from the flow manager for the next state */
|
|
94
|
-
next: Promise<FinalState | NextState
|
|
94
|
+
next: Promise<FinalState | NextState>;
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
| {
|
package/src/player.tsx
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { SyncWaterfallHook, AsyncParallelHook } from "tapable-ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Subscribe,
|
|
5
|
+
useSubscribedState,
|
|
6
|
+
useSubscriber,
|
|
7
|
+
} from "@player-ui/react-subscribe";
|
|
4
8
|
import { Registry } from "@player-ui/partial-match-registry";
|
|
5
9
|
import type {
|
|
6
|
-
A2UISnapshot,
|
|
7
10
|
CompletedState,
|
|
8
11
|
PlayerPlugin,
|
|
9
12
|
Flow,
|
|
10
13
|
View,
|
|
11
14
|
PlayerInfo,
|
|
12
|
-
StartOptions,
|
|
13
15
|
} from "@player-ui/player";
|
|
14
16
|
import { Player } from "@player-ui/player";
|
|
15
|
-
import { ErrorBoundary } from "react-error-boundary";
|
|
17
|
+
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
|
16
18
|
import type { AssetRegistryType } from "./asset";
|
|
17
19
|
import { AssetContext } from "./asset";
|
|
18
20
|
import { PlayerContext } from "./utils";
|
|
@@ -21,6 +23,10 @@ import type { ReactPlayerProps } from "./app";
|
|
|
21
23
|
import { ReactPlayer as PlayerComp } from "./app";
|
|
22
24
|
import { OnUpdatePlugin } from "./plugins/onupdate-plugin";
|
|
23
25
|
|
|
26
|
+
/** Backup context for receiving ReactPlayerComponentProps when components setup in the webComponent call don't pass the props down to their inner components. */
|
|
27
|
+
export const ReactPlayerPropsContext: React.Context<ReactPlayerComponentProps> =
|
|
28
|
+
React.createContext<ReactPlayerComponentProps>({ isInErrorState: false });
|
|
29
|
+
|
|
24
30
|
export interface DevtoolsGlobals {
|
|
25
31
|
/** A global for a plugin to load to Player for devtools */
|
|
26
32
|
__PLAYER_DEVTOOLS_PLUGIN?: {
|
|
@@ -54,7 +60,11 @@ export interface ReactPlayerOptions {
|
|
|
54
60
|
plugins?: Array<ReactPlayerPlugin>;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
export type ReactPlayerComponentProps =
|
|
63
|
+
export type ReactPlayerComponentProps = {
|
|
64
|
+
/** Whether or not player is currently recovering from an error. */
|
|
65
|
+
isInErrorState?: boolean;
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
};
|
|
58
68
|
|
|
59
69
|
/** A Player that renders UI through React */
|
|
60
70
|
export class ReactPlayer {
|
|
@@ -66,7 +76,10 @@ export class ReactPlayer {
|
|
|
66
76
|
/**
|
|
67
77
|
* A hook to create a React Component to be used for Player, regardless of the current flow state
|
|
68
78
|
*/
|
|
69
|
-
webComponent: SyncWaterfallHook<
|
|
79
|
+
webComponent: SyncWaterfallHook<
|
|
80
|
+
[React.ComponentType<any>],
|
|
81
|
+
Record<string, any>
|
|
82
|
+
>;
|
|
70
83
|
/**
|
|
71
84
|
* A hook to create a React Component that's used to render a specific view.
|
|
72
85
|
* It will be called for each view update from the core player.
|
|
@@ -99,7 +112,8 @@ export class ReactPlayer {
|
|
|
99
112
|
onBeforeViewReset: new AsyncParallelHook(),
|
|
100
113
|
};
|
|
101
114
|
|
|
102
|
-
public readonly viewUpdateSubscription
|
|
115
|
+
public readonly viewUpdateSubscription: Subscribe<View> =
|
|
116
|
+
new Subscribe<View>();
|
|
103
117
|
private reactPlayerInfo: ReactPlayerInfo;
|
|
104
118
|
|
|
105
119
|
constructor(options?: ReactPlayerOptions) {
|
|
@@ -155,11 +169,20 @@ export class ReactPlayer {
|
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
/** Register and apply [Plugin] if one with the same symbol is not already registered. */
|
|
158
|
-
public registerPlugin(plugin: ReactPlayerPlugin): void {
|
|
159
|
-
if (
|
|
172
|
+
public registerPlugin(plugin: ReactPlayerPlugin | PlayerPlugin): void {
|
|
173
|
+
if (plugin.apply) {
|
|
174
|
+
this.player.registerPlugin(plugin as PlayerPlugin);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if ((plugin as ReactPlayerPlugin).applyReact) {
|
|
178
|
+
(plugin as ReactPlayerPlugin).applyReact?.(this);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!this.options.plugins) {
|
|
182
|
+
this.options.plugins = [];
|
|
183
|
+
}
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
this.options.plugins?.push(plugin);
|
|
185
|
+
this.options.plugins.push(plugin);
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
/**
|
|
@@ -183,20 +206,118 @@ export class ReactPlayer {
|
|
|
183
206
|
|
|
184
207
|
/** Wrap the Error boundary and context provider after the hook call to catch anything wrapped by the hook */
|
|
185
208
|
const ReactPlayerComponent = (props: ReactPlayerComponentProps) => {
|
|
209
|
+
const trackedErrors = React.useRef(new Map<Error, boolean>());
|
|
210
|
+
const [errorSubId, setErrorSubId] = React.useState<number | undefined>(
|
|
211
|
+
undefined,
|
|
212
|
+
);
|
|
213
|
+
const { subscribe, unsubscribe } = useSubscriber(
|
|
214
|
+
this.viewUpdateSubscription,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const componentProps: ReactPlayerComponentProps = React.useMemo(
|
|
218
|
+
() => ({
|
|
219
|
+
...props,
|
|
220
|
+
isInErrorState: errorSubId !== undefined,
|
|
221
|
+
}),
|
|
222
|
+
[props, errorSubId],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
/** Callback to remove all tracked errors and unsub from */
|
|
226
|
+
const clearErrorTracking = React.useCallback(() => {
|
|
227
|
+
trackedErrors.current.clear();
|
|
228
|
+
setErrorSubId((prev) => {
|
|
229
|
+
if (prev !== undefined) {
|
|
230
|
+
unsubscribe(prev);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return undefined;
|
|
234
|
+
});
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
React.useEffect(() => {
|
|
238
|
+
// Clear errors and error subscription on unmount
|
|
239
|
+
return clearErrorTracking;
|
|
240
|
+
}, [clearErrorTracking]);
|
|
241
|
+
|
|
242
|
+
/** capture error and return true or false to represent if we are recovering from the error or not. */
|
|
243
|
+
const captureError = React.useCallback(
|
|
244
|
+
(err: Error) => {
|
|
245
|
+
// If player isn't in progress we can't actually render anything so render errors are irrelevant.
|
|
246
|
+
const playerState = this.player.getState();
|
|
247
|
+
if (playerState.status !== "in-progress") {
|
|
248
|
+
this.player.logger.warn(
|
|
249
|
+
`[ReactPlayer]: An error occurred during rendering but was ignored due to a change in the player state (current state: '${playerState.status}'). Error Details:`,
|
|
250
|
+
err,
|
|
251
|
+
);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Only capture each error once.
|
|
256
|
+
const currentError = trackedErrors.current.get(err);
|
|
257
|
+
if (currentError !== undefined) {
|
|
258
|
+
return currentError;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let isRecovering = false;
|
|
262
|
+
setErrorSubId((prev) => {
|
|
263
|
+
// subscribe only if no subscription available.
|
|
264
|
+
// Needs to happen before capture error to ensure error recovery isn't missed
|
|
265
|
+
const subId =
|
|
266
|
+
prev === undefined
|
|
267
|
+
? subscribe(clearErrorTracking, {
|
|
268
|
+
initializeWithPreviousValue: false,
|
|
269
|
+
})
|
|
270
|
+
: prev;
|
|
271
|
+
|
|
272
|
+
// Get skipped state after trying to capture.
|
|
273
|
+
isRecovering = playerState.controllers.error.captureError(err);
|
|
274
|
+
trackedErrors.current.set(err, isRecovering);
|
|
275
|
+
|
|
276
|
+
// If we can't recover from the error, avoid updating state to stay in error boundary
|
|
277
|
+
if (!isRecovering) {
|
|
278
|
+
// Unsub if not previously subbed since we don't need to reset the view
|
|
279
|
+
if (subId !== prev) {
|
|
280
|
+
unsubscribe(subId);
|
|
281
|
+
}
|
|
282
|
+
return prev;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return subId;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return isRecovering;
|
|
289
|
+
},
|
|
290
|
+
[errorSubId],
|
|
291
|
+
);
|
|
292
|
+
|
|
186
293
|
return (
|
|
187
294
|
<ErrorBoundary
|
|
188
|
-
fallbackRender={() =>
|
|
189
|
-
|
|
190
|
-
const playerState = this.player.getState();
|
|
295
|
+
fallbackRender={(fallbackProps: FallbackProps) => {
|
|
296
|
+
const isRecovering = captureError(fallbackProps.error);
|
|
191
297
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
298
|
+
if (!isRecovering) {
|
|
299
|
+
// Display nothing if not recovering. Let the player state fail and handle what the view will be.
|
|
300
|
+
return null;
|
|
194
301
|
}
|
|
302
|
+
fallbackProps.resetErrorBoundary();
|
|
303
|
+
|
|
304
|
+
// Render the same as on success when recovering to preserve the react tree.
|
|
305
|
+
return (
|
|
306
|
+
<ReactPlayerPropsContext.Provider
|
|
307
|
+
value={{ ...componentProps, isInErrorState: true }}
|
|
308
|
+
>
|
|
309
|
+
<PlayerContext.Provider value={{ player: this.player }}>
|
|
310
|
+
<BaseComp {...componentProps} isInErrorState />
|
|
311
|
+
</PlayerContext.Provider>
|
|
312
|
+
</ReactPlayerPropsContext.Provider>
|
|
313
|
+
);
|
|
195
314
|
}}
|
|
196
315
|
>
|
|
197
|
-
<
|
|
198
|
-
<
|
|
199
|
-
|
|
316
|
+
<ReactPlayerPropsContext.Provider value={{ ...componentProps }}>
|
|
317
|
+
<PlayerContext.Provider value={{ player: this.player }}>
|
|
318
|
+
<BaseComp {...componentProps} />
|
|
319
|
+
</PlayerContext.Provider>
|
|
320
|
+
</ReactPlayerPropsContext.Provider>
|
|
200
321
|
</ErrorBoundary>
|
|
201
322
|
);
|
|
202
323
|
};
|
|
@@ -208,17 +329,27 @@ export class ReactPlayer {
|
|
|
208
329
|
const ActualPlayerComp = this.hooks.playerComponent.call(PlayerComp);
|
|
209
330
|
|
|
210
331
|
/** the component to use to render the player */
|
|
211
|
-
const WebPlayerComponent = () => {
|
|
332
|
+
const WebPlayerComponent: React.ComponentType = (): React.ReactElement => {
|
|
333
|
+
const { isInErrorState } = React.useContext(ReactPlayerPropsContext);
|
|
212
334
|
const view = useSubscribedState<View>(this.viewUpdateSubscription);
|
|
335
|
+
const lastSuccessfulView = React.useRef<View | undefined>(undefined);
|
|
213
336
|
this.viewUpdateSubscription.suspend();
|
|
214
337
|
|
|
338
|
+
React.useEffect(() => {
|
|
339
|
+
if (!isInErrorState) {
|
|
340
|
+
lastSuccessfulView.current = view;
|
|
341
|
+
}
|
|
342
|
+
}, [isInErrorState, view]);
|
|
343
|
+
|
|
344
|
+
const displayedView = isInErrorState ? lastSuccessfulView.current : view;
|
|
345
|
+
|
|
215
346
|
return (
|
|
216
347
|
<AssetContext.Provider
|
|
217
348
|
value={{
|
|
218
349
|
registry: this.assetRegistry,
|
|
219
350
|
}}
|
|
220
351
|
>
|
|
221
|
-
{
|
|
352
|
+
{displayedView && <ActualPlayerComp view={displayedView} />}
|
|
222
353
|
</AssetContext.Provider>
|
|
223
354
|
);
|
|
224
355
|
};
|
|
@@ -238,13 +369,10 @@ export class ReactPlayer {
|
|
|
238
369
|
);
|
|
239
370
|
}
|
|
240
371
|
|
|
241
|
-
public start(
|
|
242
|
-
payload: Flow | A2UISnapshot,
|
|
243
|
-
options?: StartOptions,
|
|
244
|
-
): Promise<CompletedState> {
|
|
372
|
+
public start(flow: Flow): Promise<CompletedState> {
|
|
245
373
|
this.setWaitForNextViewUpdate();
|
|
246
374
|
|
|
247
|
-
return this.player.start(
|
|
375
|
+
return this.player.start(flow).finally(async () => {
|
|
248
376
|
await this.setWaitForNextViewUpdate();
|
|
249
377
|
});
|
|
250
378
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export
|
|
1
|
+
import { ErrorSeverity, type Asset, type PlayerErrorMetadata } from "@player-ui/player";
|
|
2
|
+
export type AssetRenderErrorMetadata = {
|
|
3
|
+
assetId: string;
|
|
4
|
+
};
|
|
5
|
+
export declare class AssetRenderError extends Error implements PlayerErrorMetadata<AssetRenderErrorMetadata> {
|
|
3
6
|
readonly rootAsset: Asset;
|
|
4
7
|
readonly innerException?: unknown | undefined;
|
|
5
8
|
private assetParentPath;
|
|
6
9
|
initialMessage: string;
|
|
7
10
|
innerExceptionMessage: string;
|
|
11
|
+
readonly type: string;
|
|
12
|
+
readonly severity: ErrorSeverity;
|
|
13
|
+
readonly metadata: AssetRenderErrorMetadata;
|
|
8
14
|
constructor(rootAsset: Asset, message?: string, innerException?: unknown | undefined);
|
|
9
15
|
private updateMessage;
|
|
10
16
|
getAssetPathMessage(): string;
|