LSPlugin.caller.ts 9.5 KB

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