mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
improve(plugin): simplify caller
This commit is contained in:
@@ -18,14 +18,12 @@
|
||||
"dompurify": "^2.2.7",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"path": "^0.12.7",
|
||||
"postmate": "^1.5.2",
|
||||
"snake-case": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/dompurify": "^2.2.1",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/postmate": "^1.5.1",
|
||||
"ts-loader": "^8.0.17",
|
||||
"typescript": "^4.2.2",
|
||||
"webpack": "^5.24.3",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Postmate from 'postmate'
|
||||
import Debug from 'debug'
|
||||
import { Postmate, Model, ParentAPI, ChildAPI } from './postmate'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import { PluginLocal } from './LSPlugin.core'
|
||||
import Debug from 'debug'
|
||||
import { deferred } from './helpers'
|
||||
import { deferred, IS_DEV } from './helpers'
|
||||
import { LSPluginShadowFrame } from './LSPlugin.shadow'
|
||||
|
||||
const debug = Debug('LSPlugin:caller')
|
||||
@@ -25,8 +25,8 @@ export const AWAIT_LSPMSGFn = (id: string) => `${FLAG_AWAIT}${id}`
|
||||
class LSPluginCaller extends EventEmitter {
|
||||
private _connected: boolean = false
|
||||
|
||||
private _parent?: Postmate.ParentAPI
|
||||
private _child?: Postmate.ChildAPI
|
||||
private _parent?: ParentAPI
|
||||
private _child?: ChildAPI
|
||||
|
||||
private _shadow?: LSPluginShadowFrame
|
||||
|
||||
@@ -87,7 +87,7 @@ class LSPluginCaller extends EventEmitter {
|
||||
},
|
||||
|
||||
[LSPMSG]: async ({ ns, type, payload }: any) => {
|
||||
debug(`${this._debugTag} [call from host]`, ns, type, payload)
|
||||
debug(`[call from host] ${this._debugTag}`, ns, type, payload)
|
||||
|
||||
if (ns && ns.startsWith('hook')) {
|
||||
caller.emit(`${ns}:${type}`, payload)
|
||||
@@ -98,7 +98,7 @@ class LSPluginCaller extends EventEmitter {
|
||||
},
|
||||
|
||||
[LSPMSG_SYNC]: ({ _sync, result }: any) => {
|
||||
debug(`sync reply #${_sync}`, result)
|
||||
debug(`[sync reply] #${_sync}`, result)
|
||||
|
||||
if (syncActors.has(_sync)) {
|
||||
const actor = syncActors.get(_sync)
|
||||
@@ -123,11 +123,12 @@ class LSPluginCaller extends EventEmitter {
|
||||
return JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()))
|
||||
}
|
||||
|
||||
const handshake = new Postmate.Model(model)
|
||||
const pm = new Model(model)
|
||||
const handshake = pm.sendHandshakeReply()
|
||||
|
||||
this._status = 'pending'
|
||||
|
||||
await handshake.then(refParent => {
|
||||
await handshake.then((refParent: ChildAPI) => {
|
||||
this._child = refParent
|
||||
this._connected = true
|
||||
|
||||
@@ -150,7 +151,7 @@ class LSPluginCaller extends EventEmitter {
|
||||
try {
|
||||
model[type](payload)
|
||||
} catch (e) {
|
||||
debug(`model method #${type} not existed`)
|
||||
debug(`[model method] #${type} not existed`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,15 +188,20 @@ class LSPluginCaller extends EventEmitter {
|
||||
}
|
||||
|
||||
async _setupIframeSandbox () {
|
||||
const cnt = document.body
|
||||
const pl = this._pluginLocal!
|
||||
const url = new URL(pl.options.entry!)
|
||||
|
||||
const handshake = new Postmate({
|
||||
container: document.body,
|
||||
url: pl.options.entry!,
|
||||
url.searchParams
|
||||
.set(`__v__`, IS_DEV ? Date.now().toString() : pl.options.version)
|
||||
|
||||
const pt = new Postmate({
|
||||
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
|
||||
@@ -206,7 +212,7 @@ class LSPluginCaller extends EventEmitter {
|
||||
reject(new Error(`handshake Timeout`))
|
||||
}, 3 * 1000) // 3secs
|
||||
|
||||
handshake.then(refChild => {
|
||||
handshake.then((refChild: ParentAPI) => {
|
||||
this._parent = refChild
|
||||
this._connected = true
|
||||
this.emit('connected')
|
||||
@@ -243,7 +249,7 @@ class LSPluginCaller extends EventEmitter {
|
||||
clearTimeout(timer)
|
||||
})
|
||||
}).catch(e => {
|
||||
debug('iframe sandbox error', e)
|
||||
debug('[iframe sandbox] error', e)
|
||||
throw e
|
||||
}).finally(() => {
|
||||
this._status = undefined
|
||||
@@ -265,7 +271,6 @@ class LSPluginCaller extends EventEmitter {
|
||||
this._call = async (type, payload = {}, actor) => {
|
||||
actor && (payload.actor = actor)
|
||||
|
||||
// TODO: support sync call
|
||||
// @ts-ignore Call in same thread
|
||||
this._pluginLocal?.emit(type, Object.assign(payload, {
|
||||
$$pid: pl.id
|
||||
@@ -289,7 +294,7 @@ class LSPluginCaller extends EventEmitter {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debug('shadow sandbox error', e)
|
||||
debug('[shadow sandbox] error', e)
|
||||
throw e
|
||||
} finally {
|
||||
this._status = undefined
|
||||
|
||||
423
libs/src/postmate/index.ts
Normal file
423
libs/src/postmate/index.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
// Fork from https://github.com/dollarshaveclub/postmate
|
||||
|
||||
/**
|
||||
* The type of messages our frames our sending
|
||||
* @type {String}
|
||||
*/
|
||||
export const messageType = 'application/x-postmate-v1+json'
|
||||
|
||||
/**
|
||||
* The maximum number of attempts to send a handshake request to the parent
|
||||
* @type {Number}
|
||||
*/
|
||||
export const maxHandshakeRequests = 5
|
||||
|
||||
/**
|
||||
* A unique message ID that is used to ensure responses are sent to the correct requests
|
||||
* @type {Number}
|
||||
*/
|
||||
let _messageId = 0
|
||||
|
||||
/**
|
||||
* Increments and returns a message ID
|
||||
* @return {Number} A unique ID for a message
|
||||
*/
|
||||
export const generateNewMessageId = () => ++_messageId
|
||||
|
||||
/**
|
||||
* Postmate logging function that enables/disables via config
|
||||
*/
|
||||
export const log = (...args) => Postmate.debug ? console.log(...args) : null
|
||||
|
||||
/**
|
||||
* Takes a URL and returns the origin
|
||||
* @param {String} url The full URL being requested
|
||||
* @return {String} The URLs origin
|
||||
*/
|
||||
export const resolveOrigin = (url) => {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol
|
||||
const host = a.host.length ? ((a.port === '80' || a.port === '443') ? a.hostname : a.host) : window.location.host
|
||||
return a.origin || `${protocol}//${host}`
|
||||
}
|
||||
|
||||
const messageTypes = {
|
||||
handshake: 1,
|
||||
'handshake-reply': 1,
|
||||
call: 1,
|
||||
emit: 1,
|
||||
reply: 1,
|
||||
request: 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a message is safe to interpret
|
||||
* @param {Object} message The postmate message being sent
|
||||
* @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export const sanitize = (message, allowedOrigin) => {
|
||||
if (
|
||||
typeof allowedOrigin === 'string' &&
|
||||
message.origin !== allowedOrigin
|
||||
) return false
|
||||
if (!message.data) return false
|
||||
if (
|
||||
typeof message.data === 'object' &&
|
||||
!('postmate' in message.data)
|
||||
) return false
|
||||
if (message.data.type !== messageType) return false
|
||||
if (!messageTypes[message.data.postmate]) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a model, and searches for a value by the property
|
||||
* @param {Object} model The dictionary to search against
|
||||
* @param {String} property A path within a dictionary (i.e. 'window.location.href')
|
||||
* passed to functions in the child model
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const resolveValue = (model, property) => {
|
||||
const unwrappedContext = typeof model[property] === 'function'
|
||||
? model[property]() : model[property]
|
||||
return Promise.resolve(unwrappedContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes an API to be used by the parent
|
||||
* @param {Object} info Information on the consumer
|
||||
*/
|
||||
export class ParentAPI {
|
||||
public parent: Window
|
||||
public frame: HTMLIFrameElement
|
||||
public child: Window
|
||||
public events = {}
|
||||
public childOrigin: string
|
||||
public listener: (e: any) => void
|
||||
|
||||
constructor (info: Postmate) {
|
||||
this.parent = info.parent
|
||||
this.frame = info.frame
|
||||
this.child = info.child
|
||||
this.childOrigin = info.childOrigin
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Registering API')
|
||||
log('Parent: Awaiting messages...')
|
||||
}
|
||||
|
||||
this.listener = (e) => {
|
||||
if (!sanitize(e, this.childOrigin)) return false
|
||||
|
||||
/**
|
||||
* the assignments below ensures that e, data, and value are all defined
|
||||
*/
|
||||
const { data, name } = (((e || {}).data || {}).value || {})
|
||||
|
||||
if (e.data.postmate === 'emit') {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log(`Parent: Received event emission: ${name}`)
|
||||
}
|
||||
if (name in this.events) {
|
||||
this.events[name].forEach(callback => {
|
||||
callback.call(this, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parent.addEventListener('message', this.listener, false)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Awaiting event emissions from Child')
|
||||
}
|
||||
}
|
||||
|
||||
get (property) {
|
||||
return new Promise((resolve) => {
|
||||
// Extract data from response and kill listeners
|
||||
const uid = generateNewMessageId()
|
||||
const transact = (e) => {
|
||||
if (e.data.uid === uid && e.data.postmate === 'reply') {
|
||||
this.parent.removeEventListener('message', transact, false)
|
||||
resolve(e.data.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare for response from Child...
|
||||
this.parent.addEventListener('message', transact, false)
|
||||
|
||||
// Then ask child for information
|
||||
this.child.postMessage({
|
||||
postmate: 'request',
|
||||
type: messageType,
|
||||
property,
|
||||
uid,
|
||||
}, this.childOrigin)
|
||||
})
|
||||
}
|
||||
|
||||
call (property, data) {
|
||||
// Send information to the child
|
||||
this.child.postMessage({
|
||||
postmate: 'call',
|
||||
type: messageType,
|
||||
property,
|
||||
data,
|
||||
}, this.childOrigin)
|
||||
}
|
||||
|
||||
on (eventName, callback) {
|
||||
if (!this.events[eventName]) {
|
||||
this.events[eventName] = []
|
||||
}
|
||||
this.events[eventName].push(callback)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Destroying Postmate instance')
|
||||
}
|
||||
window.removeEventListener('message', this.listener, false)
|
||||
this.frame.parentNode.removeChild(this.frame)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes an API to be used by the child
|
||||
* @param {Object} info Information on the consumer
|
||||
*/
|
||||
export class ChildAPI {
|
||||
private model: any
|
||||
private parent: Window
|
||||
private parentOrigin: string
|
||||
private child: Window
|
||||
|
||||
constructor (info: Model) {
|
||||
this.model = info.model
|
||||
this.parent = info.parent
|
||||
this.parentOrigin = info.parentOrigin
|
||||
this.child = info.child
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Child: Registering API')
|
||||
log('Child: Awaiting messages...')
|
||||
}
|
||||
|
||||
this.child.addEventListener('message', (e) => {
|
||||
if (!sanitize(e, this.parentOrigin)) return
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Child: Received request', e.data)
|
||||
}
|
||||
|
||||
const { property, uid, data } = e.data
|
||||
|
||||
if (e.data.postmate === 'call') {
|
||||
if (property in this.model && typeof this.model[property] === 'function') {
|
||||
this.model[property](data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Reply to Parent
|
||||
resolveValue(this.model, property)
|
||||
.then(value => {
|
||||
// @ts-ignore
|
||||
e.source.postMessage({
|
||||
property,
|
||||
postmate: 'reply',
|
||||
type: messageType,
|
||||
uid,
|
||||
value,
|
||||
}, e.origin)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emit (name, data) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log(`Child: Emitting Event "${name}"`, data)
|
||||
}
|
||||
this.parent.postMessage({
|
||||
postmate: 'emit',
|
||||
type: messageType,
|
||||
value: {
|
||||
name,
|
||||
data,
|
||||
},
|
||||
}, this.parentOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
export type PostMateOptions = {
|
||||
container: HTMLElement
|
||||
url: string
|
||||
classListArray?: Array<string>
|
||||
name?: string
|
||||
model?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point of the Parent.
|
||||
*/
|
||||
export class Postmate {
|
||||
static debug = false // eslint-disable-line no-undef
|
||||
public container?: HTMLElement
|
||||
public parent: Window
|
||||
public frame: HTMLIFrameElement
|
||||
public child?: Window
|
||||
public childOrigin?: string
|
||||
public url: string
|
||||
public model: any
|
||||
static Model: any
|
||||
|
||||
/**
|
||||
* @param opts
|
||||
*/
|
||||
constructor (opts: PostMateOptions) {
|
||||
this.container = opts.container
|
||||
this.url = opts.url
|
||||
this.parent = window
|
||||
this.frame = document.createElement('iframe')
|
||||
this.frame.name = opts.name || ''
|
||||
this.frame.classList.add.apply(this.frame.classList, opts.classListArray || [])
|
||||
this.container.appendChild(this.frame)
|
||||
this.child = this.frame.contentWindow
|
||||
this.model = opts.model || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the handshake strategy
|
||||
* @param {String} url The URL to send a handshake request to
|
||||
* @return {Promise} Promise that resolves when the handshake is complete
|
||||
*/
|
||||
sendHandshake (url?: string) {
|
||||
url = url || this.url
|
||||
const childOrigin = resolveOrigin(url)
|
||||
let attempt = 0
|
||||
let responseInterval
|
||||
return new Promise((resolve, reject) => {
|
||||
const reply = (e: any) => {
|
||||
if (!sanitize(e, childOrigin)) return false
|
||||
if (e.data.postmate === 'handshake-reply') {
|
||||
clearInterval(responseInterval)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Received handshake reply from Child')
|
||||
}
|
||||
this.parent.removeEventListener('message', reply, false)
|
||||
this.childOrigin = e.origin
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Saving Child origin', this.childOrigin)
|
||||
}
|
||||
return resolve(new ParentAPI(this))
|
||||
}
|
||||
|
||||
// Might need to remove since parent might be receiving different messages
|
||||
// from different hosts
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Invalid handshake reply')
|
||||
}
|
||||
return reject('Failed handshake')
|
||||
}
|
||||
|
||||
this.parent.addEventListener('message', reply, false)
|
||||
|
||||
const doSend = () => {
|
||||
attempt++
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin })
|
||||
}
|
||||
this.child.postMessage({
|
||||
postmate: 'handshake',
|
||||
type: messageType,
|
||||
model: this.model,
|
||||
}, childOrigin)
|
||||
|
||||
if (attempt === maxHandshakeRequests) {
|
||||
clearInterval(responseInterval)
|
||||
}
|
||||
}
|
||||
|
||||
const loaded = () => {
|
||||
doSend()
|
||||
responseInterval = setInterval(doSend, 500)
|
||||
}
|
||||
|
||||
this.frame.addEventListener('load', loaded)
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Parent: Loading frame', { url })
|
||||
}
|
||||
this.frame.src = url
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point of the Child
|
||||
*/
|
||||
export class Model {
|
||||
public child: Window
|
||||
public model: any
|
||||
public parent: Window
|
||||
public parentOrigin: string
|
||||
|
||||
/**
|
||||
* Initializes the child, model, parent, and responds to the Parents handshake
|
||||
* @param {Object} model Hash of values, functions, or promises
|
||||
* @return {Promise} The Promise that resolves when the handshake has been received
|
||||
*/
|
||||
constructor (model) {
|
||||
this.child = window
|
||||
this.model = model
|
||||
this.parent = this.child.parent
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to a handshake initiated by the Parent
|
||||
* @return {Promise} Resolves an object that exposes an API for the Child
|
||||
*/
|
||||
sendHandshakeReply () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shake = (e) => {
|
||||
if (!e.data.postmate) {
|
||||
return
|
||||
}
|
||||
if (e.data.postmate === 'handshake') {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Child: Received handshake from Parent')
|
||||
}
|
||||
this.child.removeEventListener('message', shake, false)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Child: Sending handshake reply to Parent')
|
||||
}
|
||||
e.source.postMessage({
|
||||
postmate: 'handshake-reply',
|
||||
type: messageType,
|
||||
}, e.origin)
|
||||
this.parentOrigin = e.origin
|
||||
|
||||
// Extend model with the one provided by the parent
|
||||
const defaults = e.data.model
|
||||
if (defaults) {
|
||||
Object.keys(defaults).forEach(key => {
|
||||
this.model[key] = defaults[key]
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Child: Inherited and extended model from Parent')
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
log('Child: Saving Parent origin', this.parentOrigin)
|
||||
}
|
||||
return resolve(new ChildAPI(this))
|
||||
}
|
||||
return reject('Handshake Reply Failed')
|
||||
}
|
||||
this.child.addEventListener('message', shake, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,11 @@
|
||||
// "strict": true,
|
||||
/* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": true,
|
||||
// "strictNullChecks": true,
|
||||
/* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
"strictPropertyInitialization": true,
|
||||
// "strictPropertyInitialization": true,
|
||||
/* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
700
libs/yarn.lock
700
libs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
||||
"css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production",
|
||||
"css:watch": "TAILWIND_MODE=watch postcss tailwind.all.css -o static/css/style.css --verbose --watch",
|
||||
"cljs:watch": "clojure -M:cljs watch parser-worker app electron",
|
||||
"cljs:electron-watch": "PWD=$(pwd) PATCH_PARSER_WORKER=$(cat \"./templates/patch.parser.woker.js\" | sed -e \"s#PWD_ROOT#${PWD}#g\") clojure -M:cljs watch parser-worker app electron",
|
||||
"cljs:electron-watch": "PATCH_PARSER_WORKER=$(cat \"./templates/patch.parser.woker.js\" | sed -e \"s#PWD_ROOT#`pwd`#g\") clojure -M:cljs watch parser-worker app electron",
|
||||
"cljs:release": "clojure -M:cljs release parser-worker app publishing electron",
|
||||
"cljs:test": "clojure -M:test compile test",
|
||||
"cljs:run-test": "node static/tests.js",
|
||||
|
||||
Reference in New Issue
Block a user