mirror of https://github.com/logseq/logseq
409 lines
10 KiB
TypeScript
409 lines
10 KiB
TypeScript
import Debug from 'debug'
|
|
import { Postmate, Model, ParentAPI, ChildAPI } from './postmate'
|
|
import EventEmitter from 'eventemitter3'
|
|
import { PluginLocal } from './LSPlugin.core'
|
|
import { deferred, IS_DEV } from './helpers'
|
|
import { LSPluginShadowFrame } from './LSPlugin.shadow'
|
|
|
|
const debug = Debug('LSPlugin:caller')
|
|
|
|
type DeferredActor = ReturnType<typeof deferred>
|
|
|
|
export const FLAG_AWAIT = '#await#response#'
|
|
export const LSPMSG = '#lspmsg#'
|
|
export const LSPMSG_ERROR_TAG = '#lspmsg#error#'
|
|
export const LSPMSG_SETTINGS = '#lspmsg#settings#'
|
|
export const LSPMSG_BEFORE_UNLOAD = '#lspmsg#beforeunload#'
|
|
export const LSPMSG_SYNC = '#lspmsg#reply#'
|
|
export const LSPMSG_READY = '#lspmsg#ready#'
|
|
export const LSPMSGFn = (id: string) => `${LSPMSG}${id}`
|
|
export const AWAIT_LSPMSGFn = (id: string) => `${FLAG_AWAIT}${id}`
|
|
|
|
/**
|
|
* Call between core and user
|
|
*/
|
|
class LSPluginCaller extends EventEmitter {
|
|
private _connected: boolean = false
|
|
|
|
private _parent?: ParentAPI
|
|
private _child?: ChildAPI
|
|
|
|
private _shadow?: LSPluginShadowFrame
|
|
|
|
private _status?: 'pending' | 'timeout'
|
|
private _userModel: any = {}
|
|
|
|
private _call?: (
|
|
type: string,
|
|
payload: any,
|
|
actor?: DeferredActor
|
|
) => Promise<any>
|
|
private _callUserModel?: (type: string, payload: any) => Promise<any>
|
|
|
|
private _debugTag = ''
|
|
|
|
constructor(private _pluginLocal: PluginLocal | null) {
|
|
super()
|
|
|
|
if (_pluginLocal) {
|
|
this._debugTag = _pluginLocal.debugTag
|
|
}
|
|
}
|
|
|
|
async connectToChild() {
|
|
if (this._connected) return
|
|
|
|
const { shadow } = this._pluginLocal!
|
|
|
|
if (shadow) {
|
|
await this._setupShadowSandbox()
|
|
} else {
|
|
await this._setupIframeSandbox()
|
|
}
|
|
}
|
|
|
|
// run in sandbox
|
|
async connectToParent(userModel = {}) {
|
|
if (this._connected) return
|
|
|
|
const caller = this
|
|
const isShadowMode = this._pluginLocal != null
|
|
|
|
let syncGCTimer: any = 0
|
|
let syncTag = 0
|
|
const syncActors = new Map<number, DeferredActor>()
|
|
const readyDeferred = deferred(1000 * 5)
|
|
|
|
const model: any = this._extendUserModel({
|
|
[LSPMSG_READY]: async (baseInfo) => {
|
|
// dynamically setup common msg handler
|
|
model[LSPMSGFn(baseInfo?.pid)] = ({
|
|
type,
|
|
payload,
|
|
}: {
|
|
type: string
|
|
payload: any
|
|
}) => {
|
|
debug(`[host (_call) -> *user] ${this._debugTag}`, type, payload)
|
|
// host._call without async
|
|
caller.emit(type, payload)
|
|
}
|
|
|
|
await readyDeferred.resolve()
|
|
},
|
|
|
|
[LSPMSG_BEFORE_UNLOAD]: async (e) => {
|
|
const actor = deferred(10 * 1000)
|
|
caller.emit('beforeunload', Object.assign({ actor }, e))
|
|
await actor.promise
|
|
},
|
|
|
|
[LSPMSG_SETTINGS]: async ({ type, payload }) => {
|
|
caller.emit('settings:changed', payload)
|
|
},
|
|
|
|
[LSPMSG]: async ({ ns, type, payload }: any) => {
|
|
debug(
|
|
`[host (async) -> *user] ${this._debugTag} ns=${ns} type=${type}`,
|
|
payload
|
|
)
|
|
|
|
if (ns && ns.startsWith('hook')) {
|
|
caller.emit(`${ns}:${type}`, payload)
|
|
return
|
|
}
|
|
|
|
caller.emit(type, payload)
|
|
},
|
|
|
|
[LSPMSG_SYNC]: ({ _sync, result }: any) => {
|
|
debug(`[sync host -> *user] #${_sync}`, result)
|
|
|
|
if (syncActors.has(_sync)) {
|
|
const actor = syncActors.get(_sync)
|
|
|
|
if (actor) {
|
|
if (result?.hasOwnProperty(LSPMSG_ERROR_TAG)) {
|
|
actor.reject(result[LSPMSG_ERROR_TAG])
|
|
} else {
|
|
actor.resolve(result)
|
|
}
|
|
|
|
syncActors.delete(_sync)
|
|
}
|
|
}
|
|
},
|
|
|
|
...userModel,
|
|
})
|
|
|
|
if (isShadowMode) {
|
|
await readyDeferred.promise
|
|
return JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()))
|
|
}
|
|
|
|
const pm = new Model(model)
|
|
const handshake = pm.sendHandshakeReply()
|
|
|
|
this._status = 'pending'
|
|
|
|
await handshake
|
|
.then((refParent: ChildAPI) => {
|
|
this._child = refParent
|
|
this._connected = true
|
|
|
|
this._call = async (type, payload = {}, actor) => {
|
|
if (actor) {
|
|
const tag = ++syncTag
|
|
syncActors.set(tag, actor)
|
|
payload._sync = tag
|
|
|
|
actor.setTag(`async call #${tag}`)
|
|
debug(`async call #${tag}`)
|
|
}
|
|
|
|
refParent.emit(LSPMSGFn(model.baseInfo.id), { type, payload })
|
|
|
|
return actor?.promise as Promise<any>
|
|
}
|
|
|
|
this._callUserModel = async (type, payload) => {
|
|
try {
|
|
model[type](payload)
|
|
} catch (e) {
|
|
debug(`[model method] #${type} not existed`)
|
|
}
|
|
}
|
|
|
|
// actors GC
|
|
syncGCTimer = setInterval(() => {
|
|
if (syncActors.size > 100) {
|
|
for (const [k, v] of syncActors) {
|
|
if (v.settled) {
|
|
syncActors.delete(k)
|
|
}
|
|
}
|
|
}
|
|
}, 1000 * 60 * 30)
|
|
})
|
|
.finally(() => {
|
|
this._status = undefined
|
|
})
|
|
|
|
await readyDeferred.promise
|
|
|
|
return model.baseInfo
|
|
}
|
|
|
|
async call(type: any, payload: any = {}) {
|
|
return this._call?.call(this, type, payload)
|
|
}
|
|
|
|
// only for callable apis for sdk user
|
|
async callAsync(type: any, payload: any = {}) {
|
|
const actor = deferred(1000 * 10)
|
|
return this._call?.call(this, type, payload, actor)
|
|
}
|
|
|
|
async callUserModel(type: string, payload: any = {}) {
|
|
return this._callUserModel?.call(this, type, payload)
|
|
}
|
|
|
|
// run in host
|
|
async _setupIframeSandbox() {
|
|
const pl = this._pluginLocal!
|
|
const id = pl.id
|
|
const domId = `${id}_lsp_main`
|
|
const url = new URL(pl.options.entry!)
|
|
|
|
url.searchParams.set(
|
|
`__v__`,
|
|
IS_DEV ? Date.now().toString() : pl.options.version
|
|
)
|
|
|
|
// clear zombie sandbox
|
|
const zb = document.querySelector(`#${domId}`)
|
|
if (zb) zb.parentElement.removeChild(zb)
|
|
|
|
const cnt = document.createElement('div')
|
|
cnt.classList.add('lsp-iframe-sandbox-container')
|
|
cnt.id = domId
|
|
cnt.dataset.pid = id
|
|
|
|
// TODO: apply any container layout data
|
|
try {
|
|
const mainLayoutInfo = (await this._pluginLocal._loadLayoutsData())?.$$0
|
|
if (mainLayoutInfo) {
|
|
cnt.dataset.inited_layout = 'true'
|
|
const { width, height, left, top } = mainLayoutInfo
|
|
Object.assign(cnt.style, {
|
|
width: width + 'px',
|
|
height: height + 'px',
|
|
left: left + 'px',
|
|
top: top + 'px',
|
|
})
|
|
}
|
|
} catch (e) {
|
|
console.error('[Restore Layout Error]', e)
|
|
}
|
|
|
|
document.body.appendChild(cnt)
|
|
|
|
const pt = new Postmate({
|
|
id: id + '_iframe',
|
|
container: cnt,
|
|
url: url.href,
|
|
classListArray: ['lsp-iframe-sandbox'],
|
|
model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) },
|
|
})
|
|
|
|
let handshake = pt.sendHandshake()
|
|
this._status = 'pending'
|
|
|
|
// timeout for handshake
|
|
let timer
|
|
|
|
return new Promise((resolve, reject) => {
|
|
timer = setTimeout(() => {
|
|
reject(new Error(`handshake Timeout`))
|
|
}, 8 * 1000) // 8 secs
|
|
|
|
handshake
|
|
.then((refChild: ParentAPI) => {
|
|
this._parent = refChild
|
|
this._connected = true
|
|
this.emit('connected')
|
|
|
|
refChild.on(LSPMSGFn(pl.id), ({ type, payload }: any) => {
|
|
debug(`[user -> *host] `, type, payload)
|
|
|
|
this._pluginLocal?.emit(type, payload || {})
|
|
})
|
|
|
|
this._call = async (...args: any) => {
|
|
// parent all will get message before handshake
|
|
await refChild.call(LSPMSGFn(pl.id), {
|
|
type: args[0],
|
|
payload: Object.assign(args[1] || {}, {
|
|
$$pid: pl.id,
|
|
}),
|
|
})
|
|
}
|
|
|
|
this._callUserModel = async (type, payload: any) => {
|
|
if (type.startsWith(FLAG_AWAIT)) {
|
|
// TODO: attach payload with method call
|
|
return await refChild.get(type.replace(FLAG_AWAIT, ''))
|
|
} else {
|
|
refChild.call(type, payload)
|
|
}
|
|
}
|
|
|
|
resolve(null)
|
|
})
|
|
.catch((e) => {
|
|
reject(e)
|
|
})
|
|
.finally(() => {
|
|
clearTimeout(timer)
|
|
})
|
|
})
|
|
.catch((e) => {
|
|
debug('[iframe sandbox] error', e)
|
|
throw e
|
|
})
|
|
.finally(() => {
|
|
this._status = undefined
|
|
})
|
|
}
|
|
|
|
async _setupShadowSandbox() {
|
|
const pl = this._pluginLocal!
|
|
const shadow = (this._shadow = new LSPluginShadowFrame(pl))
|
|
|
|
try {
|
|
this._status = 'pending'
|
|
|
|
await shadow.load()
|
|
|
|
this._connected = true
|
|
this.emit('connected')
|
|
|
|
this._call = async (type, payload = {}, actor) => {
|
|
actor && (payload.actor = actor)
|
|
|
|
// @ts-ignore Call in same thread
|
|
this._pluginLocal?.emit(
|
|
type,
|
|
Object.assign(payload, {
|
|
$$pid: pl.id,
|
|
})
|
|
)
|
|
|
|
return actor?.promise
|
|
}
|
|
|
|
this._callUserModel = async (...args: any) => {
|
|
let type = args[0] as string
|
|
|
|
if (type?.startsWith(FLAG_AWAIT)) {
|
|
type = type.replace(FLAG_AWAIT, '')
|
|
}
|
|
|
|
const payload = args[1] || {}
|
|
const fn = this._userModel[type]
|
|
|
|
if (typeof fn === 'function') {
|
|
await fn.call(null, payload)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debug('[shadow sandbox] error', e)
|
|
throw e
|
|
} finally {
|
|
this._status = undefined
|
|
}
|
|
}
|
|
|
|
_extendUserModel(model: any) {
|
|
return Object.assign(this._userModel, model)
|
|
}
|
|
|
|
_getSandboxIframeContainer() {
|
|
return this._parent?.frame.parentNode as HTMLDivElement
|
|
}
|
|
|
|
_getSandboxShadowContainer() {
|
|
return this._shadow?.frame.parentNode as HTMLDivElement
|
|
}
|
|
|
|
_getSandboxIframeRoot() {
|
|
return this._parent?.frame
|
|
}
|
|
|
|
_getSandboxShadowRoot() {
|
|
return this._shadow?.frame
|
|
}
|
|
|
|
set debugTag(value: string) {
|
|
this._debugTag = value
|
|
}
|
|
|
|
async destroy() {
|
|
let root: HTMLElement = null
|
|
if (this._parent) {
|
|
root = this._getSandboxIframeContainer()
|
|
await this._parent.destroy()
|
|
}
|
|
|
|
if (this._shadow) {
|
|
root = this._getSandboxShadowContainer()
|
|
this._shadow.destroy()
|
|
}
|
|
|
|
root?.parentNode.removeChild(root)
|
|
}
|
|
}
|
|
|
|
export { LSPluginCaller }
|