@player-ui/react 0.0.1-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.
package/src/player.tsx ADDED
@@ -0,0 +1,245 @@
1
+ /* eslint-disable react/no-this-in-sfc */
2
+ import React from 'react';
3
+ import type {
4
+ CompletedState,
5
+ PlayerPlugin,
6
+ Flow,
7
+ View,
8
+ } from '@player-ui/player';
9
+ import { Player } from '@player-ui/player';
10
+ import type { AssetRegistryType } from '@player-ui/react-asset';
11
+ import { AssetContext } from '@player-ui/react-asset';
12
+ import { ErrorBoundary } from 'react-error-boundary';
13
+ import { PlayerContext } from '@player-ui/react-utils';
14
+ import { SyncWaterfallHook, AsyncParallelHook } from 'tapable';
15
+ import { Subscribe, useSubscribedState } from '@player-ui/react-subscribe';
16
+ import { Registry } from '@player-ui/partial-match-registry';
17
+
18
+ import type { WebPlayerProps } from './app';
19
+ import PlayerComp from './app';
20
+ import OnUpdatePlugin from './plugins/onupdate-plugin';
21
+
22
+ const WEB_PLAYER_VERSION = '!!STABLE_VERSION!!';
23
+ const COMMIT = '!!STABLE_GIT_COMMIT!!';
24
+
25
+ export interface DevtoolsGlobals {
26
+ /** A global for a plugin to load to Player for devtools */
27
+ __PLAYER_DEVTOOLS_PLUGIN?: {
28
+ new (): WebPlayerPlugin;
29
+ };
30
+ }
31
+
32
+ export type DevtoolsWindow = typeof window & DevtoolsGlobals;
33
+
34
+ const _window: DevtoolsWindow | undefined =
35
+ typeof window === 'undefined' ? undefined : window;
36
+
37
+ export interface WebPlayerInfo {
38
+ /** Version of the running player */
39
+ playerVersion: string;
40
+
41
+ /** Version of the running webplayer */
42
+ webplayerVersion: string;
43
+
44
+ /** Hash of the HEAD commit used to build the current player version */
45
+ playerCommit: string;
46
+
47
+ /** Hash of the HEAD commit used to build the current webplayer version */
48
+ webplayerCommit: string;
49
+ }
50
+
51
+ export interface WebPlayerPlugin extends Partial<PlayerPlugin> {
52
+ /** The name of this plugin */
53
+ name: string;
54
+
55
+ /**
56
+ * Attach listeners to the web-player instance
57
+ */
58
+ applyWeb?: (webPlayer: WebPlayer) => void;
59
+ }
60
+
61
+ export interface WebPlayerOptions {
62
+ /** A headless player instance to use */
63
+ player?: Player;
64
+
65
+ /** A set of plugins to apply to this player */
66
+ plugins?: Array<WebPlayerPlugin>;
67
+
68
+ /**
69
+ * If the underlying webPlayer.Component should use `React.Suspense` to trigger a loading state while waiting for content or content updates.
70
+ * It requires that a `React.Suspense` component handler be somewhere in the `webPlayer.Component` hierarchy.
71
+ */
72
+ suspend?: boolean;
73
+ }
74
+
75
+ export type WebPlayerComponentProps = Record<string, unknown>;
76
+
77
+ /** The React webplayer */
78
+ export class WebPlayer {
79
+ public readonly options: WebPlayerOptions;
80
+ public readonly player: Player;
81
+ public readonly assetRegistry: AssetRegistryType = new Registry();
82
+ public readonly Component: React.ComponentType<WebPlayerComponentProps>;
83
+ public readonly hooks = {
84
+ /**
85
+ * A hook to create a React Component to be used for Player, regardless of the current flow state
86
+ */
87
+ webComponent: new SyncWaterfallHook<React.ComponentType>(['webComponent']),
88
+
89
+ /**
90
+ * A hook to create a React Component that's used to render a specific view.
91
+ * It will be called for each view update from the core player.
92
+ * Typically this will just be `Asset`
93
+ */
94
+ playerComponent: new SyncWaterfallHook<React.ComponentType<WebPlayerProps>>(
95
+ ['playerComponent']
96
+ ),
97
+ /**
98
+ * A hook to execute async tasks before the view resets to undefined
99
+ */
100
+ onBeforeViewReset: new AsyncParallelHook(),
101
+ };
102
+
103
+ private viewUpdateSubscription = new Subscribe<View>();
104
+ private webplayerInfo: WebPlayerInfo;
105
+
106
+ constructor(options?: WebPlayerOptions) {
107
+ this.options = options ?? {};
108
+
109
+ // Default the suspend option to `true` unless explicitly unset
110
+ // Remove the suspend option in the next major
111
+ if (!('suspend' in this.options)) {
112
+ this.options.suspend = true;
113
+ }
114
+
115
+ const Devtools = _window?.__PLAYER_DEVTOOLS_PLUGIN;
116
+ const onUpdatePlugin = new OnUpdatePlugin(
117
+ this.viewUpdateSubscription.publish
118
+ );
119
+
120
+ const plugins = options?.plugins ?? [];
121
+
122
+ if (Devtools) {
123
+ plugins.push(new Devtools());
124
+ }
125
+
126
+ const playerPlugins = plugins.filter((p) =>
127
+ Boolean(p.apply)
128
+ ) as PlayerPlugin[];
129
+
130
+ this.player = options?.player ?? new Player({ plugins: playerPlugins });
131
+
132
+ plugins.forEach((plugin) => {
133
+ if (plugin.applyWeb) {
134
+ plugin.applyWeb(this);
135
+ }
136
+ });
137
+
138
+ onUpdatePlugin.apply(this.player);
139
+
140
+ this.Component = this.hooks.webComponent.call(this.createReactComp());
141
+ this.webplayerInfo = {
142
+ playerVersion: this.player.getVersion(),
143
+ playerCommit: this.player.getCommit(),
144
+ webplayerVersion: WEB_PLAYER_VERSION,
145
+ webplayerCommit: COMMIT,
146
+ };
147
+ }
148
+
149
+ /** Returns the current version of the underlying core Player */
150
+ public getPlayerVersion(): string {
151
+ return this.webplayerInfo.playerVersion;
152
+ }
153
+
154
+ /** Returns the git commit used to build this core Player version */
155
+ public getPlayerCommit(): string {
156
+ return this.webplayerInfo.playerCommit;
157
+ }
158
+
159
+ /** Find instance of [Plugin] that has been registered to the web player */
160
+ public findPlugin<Plugin extends WebPlayerPlugin>(
161
+ symbol: symbol
162
+ ): Plugin | undefined {
163
+ return this.options.plugins?.find((el) => el.symbol === symbol) as Plugin;
164
+ }
165
+
166
+ /** Register and apply [Plugin] if one with the same symbol is not already registered. */
167
+ public registerPlugin(plugin: WebPlayerPlugin): void {
168
+ if (!plugin.applyWeb) return;
169
+
170
+ plugin.applyWeb(this);
171
+ this.options.plugins?.push(plugin);
172
+ }
173
+
174
+ /** Returns the current version of the running React Player */
175
+ public getWebPlayerVersion(): string {
176
+ return this.webplayerInfo.webplayerVersion;
177
+ }
178
+
179
+ /** Returns the git commit used to build the React Player version */
180
+ public getWebPlayerCommit(): string {
181
+ return this.webplayerInfo.webplayerCommit;
182
+ }
183
+
184
+ private createReactComp(): React.ComponentType<WebPlayerComponentProps> {
185
+ const ActualPlayerComp = this.hooks.playerComponent.call(PlayerComp);
186
+
187
+ /** the component to use to render Player */
188
+ const WebPlayerComponent = () => {
189
+ const view = useSubscribedState<View>(this.viewUpdateSubscription);
190
+
191
+ if (this.options.suspend) {
192
+ this.viewUpdateSubscription.suspend();
193
+ }
194
+
195
+ return (
196
+ <ErrorBoundary
197
+ fallbackRender={() => null}
198
+ onError={(err) => {
199
+ const playerState = this.player.getState();
200
+
201
+ if (playerState.status === 'in-progress') {
202
+ playerState.fail(err);
203
+ }
204
+ }}
205
+ >
206
+ <PlayerContext.Provider value={{ player: this.player }}>
207
+ <AssetContext.Provider
208
+ value={{
209
+ registry: this.assetRegistry,
210
+ }}
211
+ >
212
+ {view && <ActualPlayerComp view={view} />}
213
+ </AssetContext.Provider>
214
+ </PlayerContext.Provider>
215
+ </ErrorBoundary>
216
+ );
217
+ };
218
+
219
+ return WebPlayerComponent;
220
+ }
221
+
222
+ /**
223
+ * Call this method to force the WebPlayer to wait for the next view-update before performing the next render.
224
+ * If the `suspense` option is set, this will suspend while an update is pending, otherwise nothing will be rendered.
225
+ */
226
+ public setWaitForNextViewUpdate() {
227
+ // If the `suspend` option isn't set, then we need to reset immediately otherwise we risk flashing the old view while the new one is processing
228
+ const shouldCallResetHook =
229
+ this.options.suspend && this.hooks.onBeforeViewReset.isUsed();
230
+
231
+ return this.viewUpdateSubscription.reset(
232
+ shouldCallResetHook ? this.hooks.onBeforeViewReset.promise() : undefined
233
+ );
234
+ }
235
+
236
+ public start(flow: Flow): Promise<CompletedState> {
237
+ this.setWaitForNextViewUpdate();
238
+
239
+ return this.player.start(flow).finally(async () => {
240
+ if (this.options?.suspend) {
241
+ await this.setWaitForNextViewUpdate();
242
+ }
243
+ });
244
+ }
245
+ }
@@ -0,0 +1,47 @@
1
+ import type { Player, PlayerPlugin, ViewInstance } from '@player-ui/player';
2
+
3
+ export type OnUpdateCallback = (update: any) => void;
4
+
5
+ /**
6
+ * A plugin that listens for view updates and publishes an event for when a view is updated
7
+ */
8
+ export default class OnUpdatePlugin implements PlayerPlugin {
9
+ name = 'view-update';
10
+
11
+ private readonly onUpdateCallback: OnUpdateCallback;
12
+
13
+ constructor(onUpdate: OnUpdateCallback) {
14
+ this.onUpdateCallback = onUpdate;
15
+ }
16
+
17
+ apply(player: Player) {
18
+ /** Trigger the callback for the view update */
19
+ const updateTap = (updatedView: any) => {
20
+ this.onUpdateCallback(updatedView);
21
+ };
22
+
23
+ /** Trigger the callback for the view creation */
24
+ const viewTap = (view: ViewInstance) => {
25
+ view.hooks.onUpdate.tap(this.name, updateTap);
26
+ };
27
+
28
+ // Attach hooks for any new vc that gets created
29
+ player.hooks.view.tap(this.name, viewTap);
30
+
31
+ // Attach listeners and publish an update event for a view already in progress
32
+ const currentPlayerState = player.getState();
33
+
34
+ if (currentPlayerState.status === 'in-progress') {
35
+ const { currentView } = currentPlayerState.controllers.view;
36
+
37
+ if (currentView) {
38
+ viewTap(currentView);
39
+ const { lastUpdate } = currentView;
40
+
41
+ if (lastUpdate) {
42
+ this.onUpdateCallback(lastUpdate);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,20 @@
1
+ import type { PlayerFlowState, Player } from '@player-ui/player';
2
+ import type { WebPlayerPlugin } from '../player';
3
+
4
+ /**
5
+ * A plugin to tap into state transition changes and call an arbitrary update function
6
+ */
7
+ export class StateTapPlugin implements WebPlayerPlugin {
8
+ name = 'statetap';
9
+ private callbackFunction: (state: PlayerFlowState) => void;
10
+
11
+ constructor(callback: (state: PlayerFlowState) => void) {
12
+ this.callbackFunction = callback;
13
+ }
14
+
15
+ apply(player: Player) {
16
+ player.hooks.state.tap('usePlayer', (newPlayerState: PlayerFlowState) => {
17
+ this.callbackFunction(newPlayerState);
18
+ });
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ // Declaration for libraries that don't have types
2
+ declare module 'babel-plugin-preval/macro';