@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.
@@ -0,0 +1,319 @@
1
+ import React from 'react';
2
+ import type {
3
+ FlowManager,
4
+ ManagedPlayerProps,
5
+ ManagedPlayerState,
6
+ ManagerMiddleware,
7
+ ManagedPlayerContext,
8
+ } from './types';
9
+ import { useRequestTime } from './request-time';
10
+ import type { WebPlayerOptions } from '../player';
11
+ import { WebPlayer } from '../player';
12
+
13
+ /** noop middleware */
14
+ function identityMiddleware<T>(next: Promise<T>) {
15
+ return next;
16
+ }
17
+
18
+ interface ManagedPlayerStateKey {
19
+ /** the storage key for the state (outside of the react tree) */
20
+ _key: symbol;
21
+ }
22
+
23
+ export interface StateChangeCallback {
24
+ /** Trigger for state changes */
25
+ onState: (s: ManagedPlayerState) => void;
26
+ }
27
+
28
+ /**
29
+ * An object to store the state of the managed player
30
+ */
31
+ class ManagedState {
32
+ public state?: ManagedPlayerState;
33
+ private callbacks: Array<StateChangeCallback>;
34
+ private middleware?: ManagerMiddleware;
35
+
36
+ constructor({
37
+ middleware,
38
+ }: {
39
+ /** middleware to use in the managed player */
40
+ middleware?: ManagerMiddleware;
41
+ }) {
42
+ this.middleware = middleware;
43
+ this.callbacks = [];
44
+ }
45
+
46
+ /** Add a listener to state changes */
47
+ public addListener(callback: StateChangeCallback): () => void {
48
+ this.callbacks.push(callback);
49
+
50
+ return () => {
51
+ this.callbacks = this.callbacks.filter((s) => s !== callback);
52
+ };
53
+ }
54
+
55
+ /** start the managed flow */
56
+ public start(options: {
57
+ /** the flow manager to use */
58
+ manager: FlowManager;
59
+
60
+ /** the config to use when creating a player */
61
+ playerConfig: WebPlayerOptions;
62
+ }) {
63
+ const playerConfig: WebPlayerOptions = {
64
+ ...options.playerConfig,
65
+ suspend: true,
66
+ };
67
+
68
+ const initialState: ManagedPlayerState = {
69
+ value: 'not_started',
70
+ context: {
71
+ playerConfig,
72
+ webPlayer: new WebPlayer(playerConfig),
73
+ manager: options.manager,
74
+ },
75
+ };
76
+
77
+ this.setState(initialState);
78
+
79
+ return this;
80
+ }
81
+
82
+ /** reset starts from nothing */
83
+ public reset() {
84
+ if (this.state?.value === 'error') {
85
+ const { playerConfig, manager } = this.state.context;
86
+ this.start({ playerConfig, manager });
87
+ } else {
88
+ throw new Error('Flow must be in error state to reset');
89
+ }
90
+ }
91
+
92
+ /** restart starts from the last result */
93
+ public restart() {
94
+ if (this.state?.value === 'error') {
95
+ const { playerConfig, manager, prevResult, webPlayer } =
96
+ this.state.context;
97
+ this.setState({
98
+ value: 'completed',
99
+ context: {
100
+ playerConfig,
101
+ manager,
102
+ result: prevResult,
103
+ webPlayer,
104
+ },
105
+ });
106
+ } else {
107
+ throw new Error('Flow must be in error state to restart');
108
+ }
109
+ }
110
+
111
+ private async setState(state: ManagedPlayerState) {
112
+ this.state = state;
113
+ this.callbacks.forEach((c) => {
114
+ c.onState(state);
115
+ });
116
+
117
+ const { manager, webPlayer, playerConfig } = state.context;
118
+
119
+ try {
120
+ const nextState = await this.processState(state, {
121
+ manager,
122
+ webPlayer,
123
+ playerConfig,
124
+ });
125
+
126
+ if (nextState) {
127
+ this.setState(nextState);
128
+ }
129
+ } catch (e) {
130
+ this.setState({
131
+ value: 'error',
132
+ context: {
133
+ manager,
134
+ webPlayer,
135
+ playerConfig,
136
+ error: e as Error,
137
+ },
138
+ });
139
+ }
140
+ }
141
+
142
+ private async processState(
143
+ state: ManagedPlayerState,
144
+ context: ManagedPlayerContext
145
+ ): Promise<ManagedPlayerState | undefined> {
146
+ if (state.value === 'not_started' || state.value === 'completed') {
147
+ const prevResult =
148
+ state.value === 'completed' ? state.context.result : undefined;
149
+
150
+ const middleware = this.middleware?.next ?? identityMiddleware;
151
+
152
+ return {
153
+ value: 'pending',
154
+ context: {
155
+ ...context,
156
+ prevResult,
157
+ next: middleware(state.context.manager.next(prevResult)),
158
+ },
159
+ };
160
+ }
161
+
162
+ if (state.value === 'pending') {
163
+ const nextResult = await state.context.next;
164
+
165
+ if (nextResult.done) {
166
+ return {
167
+ value: 'ended',
168
+ context: {
169
+ ...context,
170
+ result: state.context.prevResult,
171
+ },
172
+ };
173
+ }
174
+
175
+ return {
176
+ value: 'loaded',
177
+ context: {
178
+ ...context,
179
+ prevResult: state.context.prevResult,
180
+ flow: nextResult.value,
181
+ },
182
+ };
183
+ }
184
+
185
+ if (state.value === 'loaded') {
186
+ return {
187
+ value: 'running',
188
+ context: {
189
+ ...context,
190
+ flow: state.context.flow,
191
+ prevResult: state.context.prevResult,
192
+ result: state.context.webPlayer.start(state.context.flow),
193
+ },
194
+ };
195
+ }
196
+
197
+ if (state.value === 'running') {
198
+ const result = await state.context.result;
199
+
200
+ return {
201
+ value: 'completed',
202
+ context: {
203
+ ...context,
204
+ result,
205
+ },
206
+ };
207
+ }
208
+ }
209
+ }
210
+
211
+ const managedPlayerStateMachines = new WeakMap<
212
+ ManagedPlayerStateKey,
213
+ ManagedState
214
+ >();
215
+
216
+ /** Creates an x-state state machine that persists when this component is no longer renders (due to Suspense) */
217
+ export const usePersistentStateMachine = (options: {
218
+ /** the flow manager to use */
219
+ manager: FlowManager;
220
+
221
+ /** Player config */
222
+ playerConfig: WebPlayerOptions;
223
+
224
+ /** Any middleware for the manager */
225
+ middleware?: ManagerMiddleware;
226
+ }) => {
227
+ const keyRef = React.useRef<ManagedPlayerStateKey>({
228
+ _key: Symbol('managed-player'),
229
+ });
230
+
231
+ const managedState =
232
+ managedPlayerStateMachines.get(keyRef.current) ??
233
+ new ManagedState({ middleware: options.middleware });
234
+ managedPlayerStateMachines.set(keyRef.current, managedState);
235
+ const [state, setState] = React.useState(managedState.state);
236
+
237
+ React.useEffect(() => {
238
+ const unsub = managedState.addListener({
239
+ onState: (s) => {
240
+ setState(s);
241
+ },
242
+ });
243
+
244
+ if (managedState.state === undefined) {
245
+ managedState.start(options);
246
+ }
247
+
248
+ return unsub;
249
+ }, []);
250
+
251
+ return { managedState, state };
252
+ };
253
+
254
+ /**
255
+ * A ManagedPlayer is a component responsible for orchestrating multi-flow experiences using Player.
256
+ * Provide a valid `FlowManager` to handle fetching the next flow.
257
+ *
258
+ * `suspense` must be enabled to wait for results in flight.
259
+ */
260
+ export const ManagedPlayer = (props: ManagedPlayerProps) => {
261
+ const { withRequestTime, RequestTimeMetricsPlugin } = useRequestTime();
262
+
263
+ const { state, managedState } = usePersistentStateMachine({
264
+ manager: props.manager,
265
+ middleware: { next: withRequestTime },
266
+ playerConfig: {
267
+ plugins: [...(props?.plugins ?? []), RequestTimeMetricsPlugin],
268
+ player: props.player,
269
+ },
270
+ });
271
+
272
+ React.useEffect(() => {
273
+ if (state?.value === 'ended') {
274
+ props.onComplete?.(state?.context.result);
275
+ } else if (state?.value === 'error') {
276
+ props.onError?.(state?.context.error);
277
+ } else if (state?.value === 'running') {
278
+ props.onStartedFlow?.();
279
+ }
280
+ }, [state]);
281
+
282
+ React.useEffect(() => {
283
+ return () => {
284
+ const playerState = state?.context.webPlayer.player.getState();
285
+
286
+ if (state?.value === 'running' && playerState?.status === 'in-progress') {
287
+ props.manager.terminate?.(playerState.controllers.data.serialize());
288
+ }
289
+ };
290
+ }, [props.manager, state?.context.webPlayer.player, state?.value]);
291
+
292
+ if (state?.value === 'error') {
293
+ if (props.fallbackComponent) {
294
+ return (
295
+ <props.fallbackComponent
296
+ reset={() => {
297
+ managedState.reset();
298
+ }}
299
+ retry={() => {
300
+ managedState.restart();
301
+ }}
302
+ error={state.context.error}
303
+ />
304
+ );
305
+ }
306
+
307
+ if (!props.onError) {
308
+ throw state.context.error;
309
+ }
310
+ }
311
+
312
+ if (state?.context.webPlayer) {
313
+ const { Component } = state.context.webPlayer;
314
+
315
+ return <Component />;
316
+ }
317
+
318
+ return null;
319
+ };
@@ -0,0 +1,63 @@
1
+ import { useCallback, useEffect, useRef, useMemo } from 'react';
2
+ import type { Player } from '@player-ui/player';
3
+ import type { MetricsCorePlugin } from '@player-ui/metrics-plugin';
4
+ import {
5
+ MetricsCorePluginSymbol,
6
+ RequestTimeWebPlugin,
7
+ } from '@player-ui/metrics-plugin';
8
+ import type { WebPlayerPlugin } from '../player';
9
+
10
+ type RequestTime = {
11
+ /** request start time */
12
+ start?: number;
13
+ /** request end time */
14
+ end?: number;
15
+ };
16
+
17
+ /** hook to time a promise and add it to the metrics plugin */
18
+ export const useRequestTime = () => {
19
+ const requestTimeRef = useRef<RequestTime>({});
20
+
21
+ useEffect(() => {
22
+ return () => {
23
+ requestTimeRef.current = {};
24
+ };
25
+ }, [requestTimeRef]);
26
+
27
+ const getRequestTime = useCallback(() => {
28
+ const { end, start } = requestTimeRef.current;
29
+
30
+ if (end && start) {
31
+ return end - start;
32
+ }
33
+ }, [requestTimeRef]);
34
+
35
+ /** wrap a promise with tracking it's time in flight */
36
+ function withRequestTime<Type>(nextPromise: Promise<Type>): Promise<Type> {
37
+ const getTime = typeof performance === 'undefined' ? Date : performance;
38
+ requestTimeRef.current = { start: getTime.now() };
39
+
40
+ return nextPromise.finally(() => {
41
+ requestTimeRef.current = {
42
+ ...requestTimeRef.current,
43
+ end: getTime.now(),
44
+ };
45
+ });
46
+ }
47
+
48
+ const RequestTimeMetricsPlugin: WebPlayerPlugin = useMemo(() => {
49
+ return {
50
+ name: 'RequestTimeMetricsPlugin',
51
+ apply(player: Player): void {
52
+ player.applyTo<MetricsCorePlugin>(
53
+ MetricsCorePluginSymbol,
54
+ (metricsCorePlugin) => {
55
+ new RequestTimeWebPlugin(getRequestTime).apply(metricsCorePlugin);
56
+ }
57
+ );
58
+ },
59
+ };
60
+ }, [getRequestTime]);
61
+
62
+ return { withRequestTime, RequestTimeMetricsPlugin };
63
+ };
@@ -0,0 +1,162 @@
1
+ import type React from 'react';
2
+ import type { CompletedState, Flow, FlowResult } from '@player-ui/player';
3
+ import type { WebPlayer, WebPlayerOptions } from '../player';
4
+
5
+ export interface FinalState {
6
+ /** Mark the iteration as complete */
7
+ done: true;
8
+ }
9
+
10
+ export interface NextState<T> {
11
+ /** Optional mark the iteration as _not_ completed */
12
+ done?: false;
13
+
14
+ /** The next value in the iteration */
15
+ value: T;
16
+ }
17
+
18
+ export interface FlowManager {
19
+ /**
20
+ * An iterator implementation that takes the result of the previous flow and returns a new one or completion marker.
21
+ *
22
+ * If `done: true` is passed, the multi-flow experience is completed.
23
+ *
24
+ * @param previousValue - The result of the previous flow.
25
+ */
26
+ next: (
27
+ previousValue?: CompletedState
28
+ ) => Promise<FinalState | NextState<Flow>>;
29
+
30
+ /**
31
+ * Called when the flow is ended early (the react tree is torn down)
32
+ * Allows clients the opportunity to save-data before destroying the tree
33
+ */
34
+ terminate?: (data?: FlowResult['data']) => void;
35
+ }
36
+
37
+ export interface FallbackProps {
38
+ /** A callback to reset the flow iteration from the start */
39
+ reset?: () => void;
40
+
41
+ /** A callback to retry the last failed segment of the iteration */
42
+ retry?: () => void;
43
+
44
+ /** The error that caused the failure */
45
+ error?: Error;
46
+ }
47
+
48
+ export interface ManagedPlayerProps extends WebPlayerOptions {
49
+ /** The manager for populating the next flows */
50
+ manager: FlowManager;
51
+
52
+ /** A callback when a flow is started */
53
+ onStartedFlow?: () => void;
54
+
55
+ /** A callback when the entire async iteration is completed */
56
+ onComplete?: (finalState?: CompletedState) => void;
57
+
58
+ /** A callback for any errors */
59
+ onError?: (e: Error) => void;
60
+
61
+ /** A component to render when there are errors */
62
+ fallbackComponent?: React.ComponentType<FallbackProps>;
63
+ }
64
+
65
+ export type ManagedPlayerContext = {
66
+ /** The flow manager */
67
+ manager: FlowManager;
68
+
69
+ /** The web-player */
70
+ webPlayer: WebPlayer;
71
+
72
+ /** The config for Player */
73
+ playerConfig: WebPlayerOptions;
74
+ };
75
+
76
+ export type ManagedPlayerState =
77
+ | {
78
+ /** The managed player hasn't started yet */
79
+ value: 'not_started';
80
+
81
+ /** The context for the managed state */
82
+ context: ManagedPlayerContext;
83
+ }
84
+ | {
85
+ /** The managed-player is awaiting a response from the flow manager */
86
+ value: 'pending';
87
+
88
+ /** The context for the managed state */
89
+ context: ManagedPlayerContext & {
90
+ /** The previous completed state */
91
+ prevResult?: CompletedState;
92
+
93
+ /** A promise from the flow manager for the next state */
94
+ next: Promise<FinalState | NextState<Flow>>;
95
+ };
96
+ }
97
+ | {
98
+ /** A flow was retrieved from the flow manager, but hasn't been processed yet */
99
+ value: 'loaded';
100
+
101
+ /** The context for the managed state */
102
+ context: ManagedPlayerContext & {
103
+ /** The next flow to load */
104
+ flow: Flow;
105
+
106
+ /** The previous completed state */
107
+ prevResult?: CompletedState;
108
+ };
109
+ }
110
+ | {
111
+ /** Player is currently executing a flow */
112
+ value: 'running';
113
+ /** The context for the managed state */
114
+ context: ManagedPlayerContext & {
115
+ /** The currently running flow */
116
+ flow: Flow;
117
+
118
+ /** A promise for the completed result */
119
+ result: Promise<CompletedState>;
120
+
121
+ /** The previous completed result */
122
+ prevResult?: CompletedState;
123
+ };
124
+ }
125
+ | {
126
+ /** Player has completed a flow */
127
+ value: 'completed';
128
+
129
+ /** The context for the managed state */
130
+ context: ManagedPlayerContext & {
131
+ /** The result of the completed flow */
132
+ result?: CompletedState;
133
+ };
134
+ }
135
+ | {
136
+ /** The entire flow iteration has completed */
137
+ value: 'ended';
138
+
139
+ /** The context for the managed state */
140
+ context: ManagedPlayerContext & {
141
+ /** The result of the last completed flow */
142
+ result?: CompletedState;
143
+ };
144
+ }
145
+ | {
146
+ /** One of the steps in the flow has resulted in an error */
147
+ value: 'error';
148
+
149
+ /** The context for the managed state */
150
+ context: ManagedPlayerContext & {
151
+ /** The error that caused the flow to halt */
152
+ error: Error;
153
+
154
+ /** the result of the last completed flow */
155
+ prevResult?: CompletedState;
156
+ };
157
+ };
158
+
159
+ export interface ManagerMiddleware {
160
+ /** Middleware for a response from the managed-player */
161
+ next?: <Type>(nextPromise: Promise<Type>) => Promise<Type>;
162
+ }