LSPlugin.caller.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. import Debug from 'debug'
  2. import { Postmate, Model, ParentAPI, ChildAPI } from './postmate'
  3. import EventEmitter from 'eventemitter3'
  4. import { PluginLocal } from './LSPlugin.core'
  5. import { deferred, IS_DEV } from './helpers'
  6. import { LSPluginShadowFrame } from './LSPlugin.shadow'
  7. const debug = Debug('LSPlugin:caller')
  8. type DeferredActor = ReturnType<typeof deferred>
  9. export const FLAG_AWAIT = '#await#response#'
  10. export const LSPMSG = '#lspmsg#'
  11. export const LSPMSG_ERROR_TAG = '#lspmsg#error#'
  12. export const LSPMSG_SETTINGS = '#lspmsg#settings#'
  13. export const LSPMSG_BEFORE_UNLOAD = '#lspmsg#beforeunload#'
  14. export const LSPMSG_SYNC = '#lspmsg#reply#'
  15. export const LSPMSG_READY = '#lspmsg#ready#'
  16. export const LSPMSGFn = (id: string) => `${LSPMSG}${id}`
  17. export const AWAIT_LSPMSGFn = (id: string) => `${FLAG_AWAIT}${id}`
  18. /**
  19. * Call between core and user
  20. */
  21. class LSPluginCaller extends EventEmitter {
  22. private _connected: boolean = false
  23. private _parent?: ParentAPI
  24. private _child?: ChildAPI
  25. private _shadow?: LSPluginShadowFrame
  26. private _status?: 'pending' | 'timeout'
  27. private _userModel: any = {}
  28. private _call?: (
  29. type: string,
  30. payload: any,
  31. actor?: DeferredActor
  32. ) => Promise<any>
  33. private _callUserModel?: (type: string, ...payloads: any[]) => Promise<any>
  34. private _debugTag = ''
  35. constructor(private _pluginLocal: PluginLocal | null) {
  36. super()
  37. if (_pluginLocal) {
  38. this._debugTag = _pluginLocal.debugTag
  39. }
  40. }
  41. // run in host
  42. async connectToChild() {
  43. if (this._connected) return
  44. const { shadow } = this._pluginLocal!
  45. if (shadow) {
  46. await this._setupShadowSandbox()
  47. } else {
  48. await this._setupIframeSandbox()
  49. }
  50. }
  51. // run in sandbox
  52. async connectToParent(userModel = {}) {
  53. if (this._connected) return
  54. const caller = this
  55. const isShadowMode = this._pluginLocal != null
  56. let syncGCTimer: any = 0
  57. let syncTag = 0
  58. const syncActors = new Map<number, DeferredActor>()
  59. const readyDeferred = deferred(1000 * 60)
  60. const model: any = this._extendUserModel({
  61. [LSPMSG_READY]: async (baseInfo) => {
  62. // dynamically setup common msg handler
  63. model[LSPMSGFn(baseInfo?.pid)] = ({
  64. type,
  65. payload,
  66. }: {
  67. type: string
  68. payload: any
  69. }) => {
  70. debug(`[host (_call) -> *user] ${this._debugTag}`, type, payload)
  71. // host._call without async
  72. caller.emit(type, payload)
  73. }
  74. await readyDeferred.resolve()
  75. },
  76. [LSPMSG_BEFORE_UNLOAD]: async (e) => {
  77. const actor = deferred(10 * 1000)
  78. caller.emit('beforeunload', Object.assign({ actor }, e))
  79. await actor.promise
  80. },
  81. [LSPMSG_SETTINGS]: async ({ type, payload }) => {
  82. caller.emit('settings:changed', payload)
  83. },
  84. [LSPMSG]: async ({ ns, type, payload }: any) => {
  85. debug(
  86. `[host (async) -> *user] ${this._debugTag} ns=${ns} type=${type}`,
  87. payload
  88. )
  89. if (ns && ns.startsWith('hook')) {
  90. caller.emit(`${ns}:${type}`, payload)
  91. return
  92. }
  93. caller.emit(type, payload)
  94. },
  95. [LSPMSG_SYNC]: ({ _sync, result }: any) => {
  96. debug(`[sync host -> *user] #${_sync}`, result)
  97. if (syncActors.has(_sync)) {
  98. const actor = syncActors.get(_sync)
  99. if (actor) {
  100. if (result?.hasOwnProperty(LSPMSG_ERROR_TAG)) {
  101. actor.reject(result[LSPMSG_ERROR_TAG])
  102. } else {
  103. actor.resolve(result)
  104. }
  105. syncActors.delete(_sync)
  106. }
  107. }
  108. },
  109. ...userModel,
  110. })
  111. if (isShadowMode) {
  112. await readyDeferred.promise
  113. return JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()))
  114. }
  115. const pm = new Model(model)
  116. const handshake = pm.sendHandshakeReply()
  117. this._status = 'pending'
  118. await handshake
  119. .then((refParent: ChildAPI) => {
  120. this._child = refParent
  121. this._connected = true
  122. this._call = async (type, payload = {}, actor) => {
  123. if (actor) {
  124. const tag = ++syncTag
  125. syncActors.set(tag, actor)
  126. payload._sync = tag
  127. actor.setTag(`async call #${tag}`)
  128. debug(`async call #${tag}`)
  129. }
  130. refParent.emit(LSPMSGFn(model.baseInfo.id), { type, payload })
  131. return actor?.promise as Promise<any>
  132. }
  133. this._callUserModel = async (type, payload) => {
  134. try {
  135. model[type](payload)
  136. } catch (e) {
  137. debug(`[model method] #${type} not existed`)
  138. }
  139. }
  140. // actors GC
  141. syncGCTimer = setInterval(() => {
  142. if (syncActors.size > 100) {
  143. for (const [k, v] of syncActors) {
  144. if (v.settled) {
  145. syncActors.delete(k)
  146. }
  147. }
  148. }
  149. }, 1000 * 60 * 30)
  150. })
  151. .finally(() => {
  152. this._status = undefined
  153. })
  154. await readyDeferred.promise
  155. return model.baseInfo
  156. }
  157. async call(type: any, payload: any = {}) {
  158. return this._call?.call(this, type, payload)
  159. }
  160. // only for callable apis for sdk user
  161. async callAsync(type: any, payload: any = {}) {
  162. const actor = deferred(1000 * 10)
  163. return this._call?.call(this, type, payload, actor)
  164. }
  165. async callUserModel(type: string, ...args: any[]) {
  166. return this._callUserModel?.apply(this, [type, ...args])
  167. }
  168. async callUserModelAsync(type: string, ...args: any[]) {
  169. type = AWAIT_LSPMSGFn(type)
  170. return this._callUserModel?.apply(this, [type, ...args])
  171. }
  172. // run in host
  173. async _setupIframeSandbox() {
  174. const pl = this._pluginLocal!
  175. const id = pl.id
  176. const domId = `${id}_lsp_main`
  177. const url = new URL(pl.options.entry!)
  178. url.searchParams.set(
  179. `__v__`,
  180. IS_DEV ? Date.now().toString() : pl.options.version
  181. )
  182. // clear zombie sandbox
  183. const zb = document.querySelector(`#${domId}`)
  184. if (zb) zb.parentElement.removeChild(zb)
  185. const cnt = document.createElement('div')
  186. cnt.classList.add('lsp-iframe-sandbox-container')
  187. cnt.id = domId
  188. cnt.dataset.pid = id
  189. // TODO: apply any container layout data
  190. try {
  191. const mainLayoutInfo = (await this._pluginLocal._loadLayoutsData())?.$$0
  192. if (mainLayoutInfo) {
  193. cnt.dataset.inited_layout = 'true'
  194. let { width, height, left, top, vw, vh } = mainLayoutInfo
  195. left = Math.max(left, 0)
  196. left =
  197. typeof vw === 'number'
  198. ? `${Math.min((left * 100) / vw, 99)}%`
  199. : `${left}px`
  200. // 45 is height of headbar
  201. top = Math.max(top, 45)
  202. top =
  203. typeof vh === 'number'
  204. ? `${Math.min((top * 100) / vh, 99)}%`
  205. : `${top}px`
  206. Object.assign(cnt.style, {
  207. width: width + 'px',
  208. height: height + 'px',
  209. left,
  210. top,
  211. })
  212. }
  213. } catch (e) {
  214. console.error('[Restore Layout Error]', e)
  215. }
  216. document.body.appendChild(cnt)
  217. const pt = new Postmate({
  218. id: id + '_iframe',
  219. container: cnt,
  220. url: url.href,
  221. classListArray: ['lsp-iframe-sandbox'],
  222. model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) },
  223. })
  224. let handshake = pt.sendHandshake()
  225. this._status = 'pending'
  226. // timeout for handshake
  227. let timer
  228. return new Promise((resolve, reject) => {
  229. timer = setTimeout(() => {
  230. reject(new Error(`handshake Timeout`))
  231. pt.destroy()
  232. }, 8 * 1000) // 8 secs
  233. handshake
  234. .then((refChild: ParentAPI) => {
  235. this._parent = refChild
  236. this._connected = true
  237. this.emit('connected')
  238. refChild.on(LSPMSGFn(pl.id), ({ type, payload }: any) => {
  239. debug(`[user -> *host] `, type, payload)
  240. this._pluginLocal?.emit(type, payload || {})
  241. this._pluginLocal?.caller.emit(type, payload || {})
  242. })
  243. this._call = async (...args: any) => {
  244. // parent all will get message before handshake
  245. refChild.call(LSPMSGFn(pl.id), {
  246. type: args[0],
  247. payload: Object.assign(args[1] || {}, {
  248. $$pid: pl.id,
  249. }),
  250. })
  251. }
  252. this._callUserModel = async (type, ...payloads: any[]) => {
  253. if (type.startsWith(FLAG_AWAIT)) {
  254. return await refChild.get(
  255. type.replace(FLAG_AWAIT, ''),
  256. ...payloads
  257. )
  258. } else {
  259. refChild.call(type, payloads?.[0])
  260. }
  261. }
  262. resolve(null)
  263. })
  264. .catch((e) => {
  265. reject(e)
  266. })
  267. .finally(() => {
  268. clearTimeout(timer)
  269. })
  270. })
  271. .catch((e) => {
  272. debug('[iframe sandbox] error', e)
  273. throw e
  274. })
  275. .finally(() => {
  276. this._status = undefined
  277. })
  278. }
  279. async _setupShadowSandbox() {
  280. const pl = this._pluginLocal!
  281. const shadow = (this._shadow = new LSPluginShadowFrame(pl))
  282. try {
  283. this._status = 'pending'
  284. await shadow.load()
  285. this._connected = true
  286. this.emit('connected')
  287. this._call = async (type, payload = {}, actor) => {
  288. actor && (payload.actor = actor)
  289. // @ts-ignore Call in same thread
  290. this._pluginLocal?.emit(
  291. type,
  292. Object.assign(payload, {
  293. $$pid: pl.id,
  294. })
  295. )
  296. return actor?.promise
  297. }
  298. this._callUserModel = async (...args: any) => {
  299. let type = args[0] as string
  300. if (type?.startsWith(FLAG_AWAIT)) {
  301. type = type.replace(FLAG_AWAIT, '')
  302. }
  303. const payload = args[1] || {}
  304. const fn = this._userModel[type]
  305. if (typeof fn === 'function') {
  306. await fn.call(null, payload)
  307. }
  308. }
  309. } catch (e) {
  310. debug('[shadow sandbox] error', e)
  311. throw e
  312. } finally {
  313. this._status = undefined
  314. }
  315. }
  316. _extendUserModel(model: any) {
  317. return Object.assign(this._userModel, model)
  318. }
  319. _getSandboxIframeContainer() {
  320. return this._parent?.frame.parentNode as HTMLDivElement
  321. }
  322. _getSandboxShadowContainer() {
  323. return this._shadow?.frame.parentNode as HTMLDivElement
  324. }
  325. _getSandboxIframeRoot() {
  326. return this._parent?.frame
  327. }
  328. _getSandboxShadowRoot() {
  329. return this._shadow?.frame
  330. }
  331. set debugTag(value: string) {
  332. this._debugTag = value
  333. }
  334. async destroy() {
  335. let root: HTMLElement = null
  336. if (this._parent) {
  337. root = this._getSandboxIframeContainer()
  338. await this._parent.destroy()
  339. }
  340. if (this._shadow) {
  341. root = this._getSandboxShadowContainer()
  342. this._shadow.destroy()
  343. }
  344. root?.parentNode.removeChild(root)
  345. }
  346. }
  347. export { LSPluginCaller }