Skip to content

Commit

Permalink
feat(plugin): merge implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
xyhp915 committed May 7, 2021
1 parent 7c8c82a commit 7f86723
Show file tree
Hide file tree
Showing 20 changed files with 4,292 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ strings.csv
resources/electron.js
.clj-kondo/
.lsp/
/libs/dist/
3 changes: 2 additions & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
thheller/shadow-cljs {:mvn/version "2.12.5"}
expound/expound {:mvn/version "0.8.6"}
com.lambdaisland/glogi {:mvn/version "1.0.116"}
binaryage/devtools {:mvn/version "1.0.2"}}
binaryage/devtools {:mvn/version "1.0.2"}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"}}

:aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.844"}
Expand Down
2 changes: 2 additions & 0 deletions libs/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/
webpack.*
17 changes: 17 additions & 0 deletions libs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## @logseq/libs

🚀 Logseq SDK libraries [WIP].

#### Installation

```shell
yarn add @logseq/libs
```

#### Usage

Load `logseq` plugin sdk as global namespace

```js
import "@logseq/libs"
```
5 changes: 5 additions & 0 deletions libs/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ILSPluginUser } from './dist/LSPlugin'

declare global {
var logseq: ILSPluginUser
}
34 changes: 34 additions & 0 deletions libs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@logseq/libs",
"version": "0.0.1-alpha.6",
"description": "Logseq SDK libraries",
"main": "dist/lsplugin.user.js",
"typings": "index.d.ts",
"private": false,
"scripts": {
"build:user": "webpack --mode production",
"dev:user": "npm run build:user -- --mode development --watch",
"build:core": "webpack --config webpack.config.core.js --mode production",
"dev:core": "npm run build:core -- --mode development --watch",
"build": "tsc && rm dist/*.js && cp src/*.d.ts dist/ && npm run build:user"
},
"dependencies": {
"debug": "^4.3.1",
"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",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0"
}
}
286 changes: 286 additions & 0 deletions libs/src/LSPlugin.caller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import Postmate from 'postmate'
import EventEmitter from 'eventemitter3'
import { PluginLocal } from './LSPlugin.core'
import Debug from 'debug'
import { deferred } from './helpers'
import { LSPluginShadowFrame } from './LSPlugin.shadow'

const debug = Debug('LSPlugin:caller')

type DeferredActor = ReturnType<typeof deferred>

export const LSPMSG = '#lspmsg#'
export const LSPMSG_SETTINGS = '#lspmsg#settings#'
export const LSPMSG_SYNC = '#lspmsg#reply#'
export const LSPMSG_READY = '#lspmsg#ready#'
export const LSPMSGFn = (id: string) => `${LSPMSG}${id}`

/**
* Call between core and user
*/
class LSPluginCaller extends EventEmitter {
private _connected: boolean = false

private _parent?: Postmate.ParentAPI
private _child?: Postmate.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>

constructor (
private _pluginLocal: PluginLocal | null
) {
super()
}

async connectToChild () {
if (this._connected) return

const { shadow } = this._pluginLocal!

if (shadow) {
await this._setupShadowSandbox()
} else {
await this._setupIframeSandbox()
}
}

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()

const model: any = this._extendUserModel({
[LSPMSG_READY]: async () => {
await readyDeferred.resolve()
},

[LSPMSG_SETTINGS]: async ({ type, payload }) => {
caller.emit('settings:changed', payload)
},

[LSPMSG]: async ({ ns, type, payload }: any) => {
debug(`[call from host #${this._pluginLocal?.id}]`, ns, type, payload)

if (ns && ns.startsWith('hook')) {
caller.emit(`${ns}:${type}`, payload)
return
}

caller.emit(type, payload)
},

[LSPMSG_SYNC]: ({ _sync, result }: any) => {
debug(`sync reply #${_sync}`, result)
if (syncActors.has(_sync)) {
// TODO: handle exception
syncActors.get(_sync)?.resolve(result)
syncActors.delete(_sync)
}
},

...userModel
})

if (isShadowMode) {
await readyDeferred.promise
return JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()))
}

const handshake = new Postmate.Model(model)

this._status = 'pending'

await handshake.then(refParent => {
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
})

// TODO: timeout
await readyDeferred.promise

return model.baseInfo
}

async call (type: any, payload: any = {}) {
// TODO: ?
this.emit(type, payload)
return this._call?.call(this, type, payload)
}

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)
}

async _setupIframeSandbox () {
const pl = this._pluginLocal!

const handshake = new Postmate({
container: document.body,
url: pl.options.entry!,
classListArray: ['lsp-iframe-sandbox'],
model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) }
})

this._status = 'pending'

// timeout for handshake
let timer

return new Promise((resolve, reject) => {
timer = setTimeout(() => {
reject(new Error(`handshake Timeout`))
}, 3 * 1000) // 3secs

handshake.then(refChild => {
this._parent = refChild
this._connected = true
this.emit('connected')

refChild.frame.setAttribute('id', pl.id)
refChild.on(LSPMSGFn(pl.id), ({ type, payload }: any) => {
debug(`[call from plugin] `, type, payload)

this._pluginLocal?.emit(type, payload || {})
})

this._call = async (...args: any) => {
// parent all will get message
await refChild.call(LSPMSGFn(pl.id), { type: args[0], payload: args[1] || {} })
}

this._callUserModel = async (...args: any) => {
await refChild.call(args[0], args[1] || {})
}

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)

// TODO: support sync call
// @ts-ignore Call in same thread
this._pluginLocal?.emit(type, payload)

return actor?.promise
}

this._callUserModel = async (...args: any) => {
const type = args[0]
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
}

_getSandboxShadowContainer () {
return this._shadow?.frame
}

async destroy () {
if (this._parent) {
await this._parent.destroy()
}

if (this._shadow) {
this._shadow.destroy()
}
}
}

export {
LSPluginCaller
}
Loading

0 comments on commit 7f86723

Please sign in to comment.