@blocklet/launcher-workflow 2.4.4 → 2.4.5

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 (39) hide show
  1. package/es/components/in-progress-session.js +1 -1
  2. package/es/components/launch-serverless/allocate.js +181 -0
  3. package/es/components/launch-serverless/install.js +203 -0
  4. package/es/components/launch-serverless/shared/base-serverless-layout.js +53 -0
  5. package/es/components/launch-serverless/shared/common-components.js +605 -0
  6. package/es/components/launch-serverless/shared/loading-display-layout.js +122 -0
  7. package/es/components/launch-serverless/shared/retry-error-message.js +45 -0
  8. package/es/components/launch-serverless/start-app.js +356 -0
  9. package/es/contexts/request.js +2 -2
  10. package/es/hooks/use-serial-polling.js +43 -0
  11. package/es/install.js +28 -0
  12. package/es/launch.js +1 -1
  13. package/es/locales/en.js +71 -14
  14. package/es/locales/zh.js +68 -12
  15. package/es/paid.js +1 -1
  16. package/es/prepare.js +1 -1
  17. package/es/start-app.js +28 -0
  18. package/es/util.js +181 -2
  19. package/lib/components/in-progress-session.js +3 -3
  20. package/lib/components/launch-serverless/allocate.js +198 -0
  21. package/lib/components/launch-serverless/install.js +223 -0
  22. package/lib/components/launch-serverless/shared/base-serverless-layout.js +59 -0
  23. package/lib/components/launch-serverless/shared/common-components.js +635 -0
  24. package/lib/components/launch-serverless/shared/loading-display-layout.js +131 -0
  25. package/lib/components/launch-serverless/shared/retry-error-message.js +52 -0
  26. package/lib/components/launch-serverless/start-app.js +369 -0
  27. package/lib/contexts/request.js +2 -2
  28. package/lib/hooks/use-serial-polling.js +49 -0
  29. package/lib/install.js +35 -0
  30. package/lib/launch.js +2 -2
  31. package/lib/locales/en.js +71 -14
  32. package/lib/locales/zh.js +68 -12
  33. package/lib/paid.js +2 -2
  34. package/lib/prepare.js +2 -2
  35. package/lib/start-app.js +35 -0
  36. package/lib/util.js +214 -11
  37. package/package.json +8 -5
  38. package/es/components/launch-serverless.js +0 -115
  39. package/lib/components/launch-serverless.js +0 -89
@@ -5,7 +5,7 @@ import Box from '@mui/material/Box';
5
5
  import Link from '@mui/material/Link';
6
6
  import { useState } from 'react';
7
7
  import useAsync from 'react-use/lib/useAsync';
8
- import joinURL from 'url-join';
8
+ import { joinURL } from 'ufo';
9
9
  import { useLocaleContext } from '../contexts/locale';
10
10
  import useRequest from '../contexts/request';
11
11
  import { useWorkflowContext } from '../contexts/workflow';
@@ -0,0 +1,181 @@
1
+ import { useSetState } from 'ahooks';
2
+ import PropTypes from 'prop-types';
3
+ import React, { useEffect, useMemo, useRef } from 'react';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { LAUNCH_STATUS } from '@blocklet/launcher-util/lib/constant';
6
+ import { useLocaleContext } from '../../contexts/locale';
7
+ import useRequest from '../../contexts/request';
8
+ import BaseServerlessLayout from './shared/base-serverless-layout';
9
+ import { useDisplayProgress, useElapsedTime, useFormatTime } from './shared/common-components';
10
+ import LoadingDisplayLayout from './shared/loading-display-layout';
11
+ import RetryErrorMessage from './shared/retry-error-message';
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ export default function LaunchServerless({
14
+ sessionId
15
+ }) {
16
+ const {
17
+ t
18
+ } = useLocaleContext();
19
+ const {
20
+ api
21
+ } = useRequest();
22
+ const navigate = useNavigate();
23
+ const timerRef = useRef({});
24
+ const [state, setState] = useSetState({
25
+ allocating: true,
26
+ allocated: false,
27
+ error: '',
28
+ launchSession: null
29
+ });
30
+ const formatTime = useFormatTime();
31
+ const time = useElapsedTime(0);
32
+ const actions = useMemo(() => {
33
+ // 基础步骤时间
34
+ const steps = [{
35
+ message: t('launch.waiting.starting'),
36
+ time: 1
37
+ }, {
38
+ message: t('launch.waiting.securing'),
39
+ time: 1
40
+ }, {
41
+ message: t('launch.waiting.prepare'),
42
+ time: 1
43
+ }, {
44
+ message: t('launch.waiting.waiting'),
45
+ time: 1
46
+ }, {
47
+ message: t('launch.waiting.done'),
48
+ time: 1
49
+ }];
50
+
51
+ // 计算总时间
52
+ const totalTime = steps.reduce((acc, step) => acc + step.time, 0);
53
+
54
+ // 计算每个步骤的进度区间
55
+ let currentProgress = 0;
56
+ return steps.map(step => {
57
+ const progressPercent = step.time / totalTime * 100;
58
+ const range = [Math.round(currentProgress), Math.round(currentProgress + progressPercent)];
59
+ currentProgress += progressPercent;
60
+ return {
61
+ ...step,
62
+ range
63
+ };
64
+ });
65
+ }, [t]);
66
+ const estimatedTime = useMemo(() => {
67
+ return actions.reduce((acc, action) => acc + action.time, 0);
68
+ }, [actions]);
69
+ const displayProgress = useDisplayProgress(0, estimatedTime);
70
+
71
+ // 根据当前进度获取对应的 action
72
+ const getCurrentAction = progress => {
73
+ const action = actions.find(({
74
+ range: [start, end]
75
+ }) => progress >= start && progress < end);
76
+ return action?.message || actions[actions.length - 1].message;
77
+ };
78
+ useEffect(() => {
79
+ const fetch = async () => {
80
+ try {
81
+ const {
82
+ data: {
83
+ launch: launchSession
84
+ }
85
+ } = await api.get(`/launches/${sessionId}?health=1`);
86
+ setState({
87
+ launchSession
88
+ });
89
+ if (launchSession.status >= LAUNCH_STATUS.installed) {
90
+ navigate(`/start-app/${sessionId}${window.location.search || ''}`);
91
+ } else if (launchSession.status >= LAUNCH_STATUS.allocated) {
92
+ navigate(`/install/${sessionId}${window.location.search || ''}`);
93
+ } else {
94
+ await Promise.all([api.post('/serverless/allocate', {
95
+ launchId: sessionId
96
+ }), new Promise(resolve => {
97
+ timerRef.current.raceTimer = setTimeout(() => {
98
+ resolve();
99
+ }, estimatedTime * 1000);
100
+ })]);
101
+ setState({
102
+ allocating: false,
103
+ allocated: true
104
+ });
105
+ }
106
+ } catch (error) {
107
+ setState({
108
+ error: error.message,
109
+ allocating: false
110
+ });
111
+ console.error(error);
112
+ }
113
+ };
114
+ if (window.blocklet.DEVELOPER_WORKFLOW_UI) {
115
+ timerRef.current.mockTimer = setTimeout(() => {
116
+ navigate(`/install/${sessionId}${window.location.search || ''}`);
117
+ }, 5000);
118
+ } else {
119
+ fetch();
120
+ }
121
+ return () => {
122
+ const timers = Object.values(timerRef.current); // eslint-disable-line react-hooks/exhaustive-deps
123
+ timers.forEach(timer => clearTimeout(timer));
124
+ };
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, []);
127
+ useEffect(() => {
128
+ if (state.allocated) {
129
+ const timer = setTimeout(() => {
130
+ navigate(`/install/${sessionId}${window.location.search || ''}`);
131
+ }, 1000);
132
+ return () => clearTimeout(timer);
133
+ }
134
+ return () => {};
135
+ }, [state.allocated, sessionId, navigate]);
136
+ const handleRetry = async () => {
137
+ try {
138
+ setState({
139
+ error: '',
140
+ allocating: true
141
+ });
142
+ if (!state.launchSession?.status || state.launchSession.status < LAUNCH_STATUS.allocated) {
143
+ await api.post('/serverless/allocate', {
144
+ launchId: sessionId
145
+ });
146
+ } else {
147
+ await api.post('/serverless/install', {
148
+ launchId: sessionId
149
+ });
150
+ }
151
+ setState({
152
+ allocating: false,
153
+ allocated: true
154
+ });
155
+ } catch (error) {
156
+ setState({
157
+ error: error.message,
158
+ allocating: false
159
+ });
160
+ }
161
+ };
162
+ return /*#__PURE__*/_jsxs(BaseServerlessLayout, {
163
+ title: t('launch.pageTitle'),
164
+ children: [!state.error && /*#__PURE__*/_jsx(LoadingDisplayLayout, {
165
+ title: t('launch.pageTitle'),
166
+ progress: state.allocated ? 100 : Math.max(0, Math.min(displayProgress, 100)),
167
+ elapsedTime: formatTime(time),
168
+ estimatedTime: formatTime(estimatedTime),
169
+ currentAction: getCurrentAction(displayProgress),
170
+ error: null,
171
+ onRetry: null
172
+ }), state.error && /*#__PURE__*/_jsx(RetryErrorMessage, {
173
+ title: t('prepare.serverless.prepareFailed'),
174
+ onRetry: handleRetry,
175
+ retryText: t('common.retry')
176
+ })]
177
+ });
178
+ }
179
+ LaunchServerless.propTypes = {
180
+ sessionId: PropTypes.string.isRequired
181
+ };
@@ -0,0 +1,203 @@
1
+ import { useSetState } from 'ahooks';
2
+ import PropTypes from 'prop-types';
3
+ import React, { useEffect, useMemo } from 'react';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { useLocaleContext } from '../../contexts/locale';
6
+ import useRequest from '../../contexts/request';
7
+ import useSerialPolling from '../../hooks/use-serial-polling';
8
+ import BaseServerlessLayout from './shared/base-serverless-layout';
9
+ import { calculateEstimatedTime, useDisplayProgress, useElapsedTime, useFormatTime } from './shared/common-components';
10
+ import LoadingDisplayLayout from './shared/loading-display-layout';
11
+ import RetryErrorMessage from './shared/retry-error-message';
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const CHECK_INTERVAL = 2000;
14
+ const STATUS = {
15
+ installing: 1,
16
+ installed: 2,
17
+ error: 10
18
+ };
19
+ export default function LaunchServerless({
20
+ sessionId
21
+ }) {
22
+ const {
23
+ t
24
+ } = useLocaleContext();
25
+ const {
26
+ api
27
+ } = useRequest();
28
+ const navigate = useNavigate();
29
+ const [state, setState] = useSetState({
30
+ status: STATUS.installing,
31
+ startTime: Date.now(),
32
+ hasTryInstall: false,
33
+ retryCount: 0,
34
+ error: ''
35
+ });
36
+ const formatTime = useFormatTime();
37
+
38
+ // 定义带进度区间的 actions
39
+ const actions = useMemo(() => {
40
+ // 基础步骤时间
41
+ const steps = [{
42
+ message: t('install.waiting.verifying'),
43
+ time: 2
44
+ }, {
45
+ message: t('install.waiting.downloading'),
46
+ time: 5
47
+ }, {
48
+ message: t('install.waiting.extracting'),
49
+ time: 2
50
+ }, {
51
+ message: t('install.waiting.installing'),
52
+ time: 3
53
+ }, {
54
+ message: t('install.waiting.installed'),
55
+ time: 1
56
+ }];
57
+
58
+ // 计算总时间
59
+ const totalTime = steps.reduce((acc, step) => acc + step.time, 0);
60
+
61
+ // 计算每个步骤的进度区间
62
+ let currentProgress = 0;
63
+ return steps.map(step => {
64
+ const progressPercent = step.time / totalTime * 100;
65
+ const range = [Math.round(currentProgress), Math.round(currentProgress + progressPercent)];
66
+ currentProgress += progressPercent;
67
+ return {
68
+ ...step,
69
+ range
70
+ };
71
+ });
72
+ }, [t]);
73
+
74
+ // 根据组件计算预估时间
75
+ const estimatedTime = useMemo(() => {
76
+ const components = window.blockletMeta?.components || [];
77
+ const additionalTime = calculateEstimatedTime(components);
78
+ return actions.reduce((acc, {
79
+ time
80
+ }) => acc + time, 0) + Math.ceil(additionalTime * 0.6);
81
+ }, [actions]);
82
+
83
+ // 根据当前进度获取对应的 action
84
+ const getCurrentAction = progress => {
85
+ const action = actions.find(({
86
+ range: [start, end]
87
+ }) => progress >= start && progress < end);
88
+ return action?.message || actions[actions.length - 1].message;
89
+ };
90
+
91
+ // 时间管理 - 参考 launch-dedicated.js
92
+ let installStartTime = sessionStorage.getItem(`launcher-install-${sessionId}-time`);
93
+ if (!installStartTime) {
94
+ installStartTime = Date.now();
95
+ sessionStorage.setItem(`launcher-install-${sessionId}-time`, installStartTime);
96
+ }
97
+ const time = useElapsedTime(Math.round((Date.now() - installStartTime) / 1000));
98
+ const startProgress = useMemo(() => {
99
+ return Math.min(time / estimatedTime * 100, 90);
100
+ }, [time, estimatedTime]);
101
+ const displayProgress = useDisplayProgress(startProgress, state.status === STATUS.installed ? Math.min(estimatedTime - time, 2) : estimatedTime);
102
+ const checkLaunchSession = async () => {
103
+ if (window.blocklet.DEVELOPER_WORKFLOW_UI) {
104
+ // 开发模式下的模拟逻辑
105
+ setTimeout(() => {
106
+ setState({
107
+ status: STATUS.installed
108
+ });
109
+ sessionStorage.removeItem(`launcher-install-${sessionId}-time`);
110
+ navigate(`/start-app/${sessionId}${window.location.search || ''}`);
111
+ }, 47000);
112
+ return;
113
+ }
114
+ try {
115
+ const {
116
+ data: {
117
+ launch: launchSession
118
+ }
119
+ } = await api.get(`/launches/${sessionId}?health=1`);
120
+ const {
121
+ status
122
+ } = launchSession.metadata;
123
+ const hasInstalled = ['starting', 'installed', 'running', 'stopped'].includes(status);
124
+ if (launchSession.running || hasInstalled) {
125
+ sessionStorage.removeItem(`launcher-install-${sessionId}-time`);
126
+ setState({
127
+ status: STATUS.installed
128
+ });
129
+ if (['installed', 'stopped'].includes(status)) {
130
+ await api.post(`/launches/${sessionId}/start`).catch(error => {
131
+ console.error(error);
132
+ });
133
+ }
134
+ } else if (!state.hasTryInstall && !hasInstalled && Date.now() - state.startTime > 20000) {
135
+ setState({
136
+ hasTryInstall: true
137
+ });
138
+ await api.post('/serverless/install', {
139
+ launchId: sessionId
140
+ }).catch(error => {
141
+ console.error(error);
142
+ });
143
+ }
144
+ setState({
145
+ retryCount: 0
146
+ });
147
+ } catch (error) {
148
+ if (state.retryCount < 5) {
149
+ console.warn('check launch session occurred error, retry', state.retryCount, error);
150
+ setState({
151
+ retryCount: state.retryCount + 1
152
+ });
153
+ } else {
154
+ sessionStorage.removeItem(`launcher-install-${sessionId}-time`);
155
+ setState({
156
+ error: error.message,
157
+ status: STATUS.error,
158
+ retryCount: 0
159
+ });
160
+ }
161
+ }
162
+ };
163
+
164
+ // 串行检查安装状态
165
+ useSerialPolling({
166
+ isEnabled: state.status === STATUS.installing,
167
+ interval: CHECK_INTERVAL,
168
+ onPoll: checkLaunchSession
169
+ });
170
+ useEffect(() => {
171
+ if (state.status === STATUS.installed && displayProgress >= 99) {
172
+ const timer = setTimeout(() => {
173
+ sessionStorage.removeItem(`launcher-install-${sessionId}-time`);
174
+ navigate(`/start-app/${sessionId}${window.location.search || ''}`);
175
+ }, 1500);
176
+ return () => clearTimeout(timer);
177
+ }
178
+ return () => {};
179
+ }, [state.status, sessionId, navigate, displayProgress]);
180
+ const handleRetry = () => {
181
+ sessionStorage.removeItem(`launcher-install-${sessionId}-time`);
182
+ navigate(`/launch/${sessionId}${window.location.search || ''}`);
183
+ };
184
+ return /*#__PURE__*/_jsxs(BaseServerlessLayout, {
185
+ title: t('install.pageTitle'),
186
+ children: [state.status !== STATUS.error && /*#__PURE__*/_jsx(LoadingDisplayLayout, {
187
+ title: t('install.pageTitle'),
188
+ progress: state.status === STATUS.installed && displayProgress >= 99 ? 100 : displayProgress,
189
+ elapsedTime: formatTime(time),
190
+ estimatedTime: formatTime(estimatedTime),
191
+ currentAction: getCurrentAction(displayProgress),
192
+ error: null,
193
+ onRetry: handleRetry
194
+ }), state.status === STATUS.error && /*#__PURE__*/_jsx(RetryErrorMessage, {
195
+ title: t('install.installFailed'),
196
+ onRetry: handleRetry,
197
+ retryText: t('common.retry')
198
+ })]
199
+ });
200
+ }
201
+ LaunchServerless.propTypes = {
202
+ sessionId: PropTypes.string.isRequired
203
+ };
@@ -0,0 +1,53 @@
1
+ import { Box } from '@mui/material';
2
+ import PropTypes from 'prop-types';
3
+ import React from 'react';
4
+
5
+ /**
6
+ * 无服务器启动流程的基础布局组件
7
+ * 提取公共的样式和结构,避免重复代码
8
+ */
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ export default function BaseServerlessLayout({
11
+ title,
12
+ children
13
+ }) {
14
+ return /*#__PURE__*/_jsx(Box, {
15
+ sx: {
16
+ display: 'flex',
17
+ flexDirection: 'column',
18
+ width: '100%',
19
+ height: '100%',
20
+ paddingTop: '100px',
21
+ '& .page-logo': {
22
+ display: 'flex',
23
+ justifyContent: 'center',
24
+ marginTop: {
25
+ xs: '48px',
26
+ md: '62px'
27
+ }
28
+ }
29
+ },
30
+ children: /*#__PURE__*/_jsx(Box, {
31
+ sx: {
32
+ display: 'flex',
33
+ alignItems: 'center',
34
+ flexDirection: 'column',
35
+ textAlign: 'center',
36
+ '& .loading-description': {
37
+ marginTop: '8px'
38
+ },
39
+ '& .result-title': {
40
+ fontSize: '18px'
41
+ },
42
+ '& .result-title.color-loading': {
43
+ color: 'primary.main'
44
+ }
45
+ },
46
+ children: children
47
+ })
48
+ }, title);
49
+ }
50
+ BaseServerlessLayout.propTypes = {
51
+ title: PropTypes.string.isRequired,
52
+ children: PropTypes.node.isRequired
53
+ };