From bb77703941ebef0ec94794cd2a38b92900452df1 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 15 Dec 2023 21:08:54 +0530 Subject: [PATCH] docs: for NodeConnector API docs --- docs/generatedApiDocs/GitHub-API-Index.md | 1 + docs/generatedApiDocs/NodeConnector-API.md | 155 +++++++++++++++++ .../utils/EventDispatcher-API.md | 13 +- src-node/node-connector.js | 3 + src-node/test-connection.js | 4 + src/NodeConnector.js | 161 ++++++++++++++++++ src/brackets.js | 1 + src/node-loader.js | 3 + test/SpecRunner.js | 3 + test/spec/NodeConnection-test.js | 11 +- 10 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 docs/generatedApiDocs/NodeConnector-API.md create mode 100644 src/NodeConnector.js diff --git a/docs/generatedApiDocs/GitHub-API-Index.md b/docs/generatedApiDocs/GitHub-API-Index.md index 5a9ce1a346..4810815f5e 100644 --- a/docs/generatedApiDocs/GitHub-API-Index.md +++ b/docs/generatedApiDocs/GitHub-API-Index.md @@ -5,6 +5,7 @@ The list of all APIs for phoenix. 1. [features/NewFileContentManager](NewFileContentManager-API) 1. [features/QuickViewManager](QuickViewManager-API) 1. [features/SelectionViewManager](SelectionViewManager-API) +1. [NodeConnector](NodeConnector-API) 1. [utils/EventDispatcher](EventDispatcher-API) 1. [utils/EventManager](EventManager-API) 1. [utils/ExtensionInterface](ExtensionInterface-API) diff --git a/docs/generatedApiDocs/NodeConnector-API.md b/docs/generatedApiDocs/NodeConnector-API.md new file mode 100644 index 0000000000..9ba8f84a35 --- /dev/null +++ b/docs/generatedApiDocs/NodeConnector-API.md @@ -0,0 +1,155 @@ + + +## NodeConnector + +Use this module to communicate easily with node and vice versa. A `NodeConnector` is an intermediary between a +module in node and a module in phcode. With a `NodeConnector` interface, you can execute a function in node +from phoenix by simply calling `await nodeConnectorObject.execPeer("namedFunctionInNodeModule, argObject")` and +the resolved promise will have the result. No need to do any complex IPC or anything else. See a step by step +example below on how this is done. + +## The setup + +Assume that you have a module in phcode `x.js` and another module in node `y.js`. To communicate between `x` and `y` +we have to first create a `NodeConnector` on both sides. A `NodeConnector` will have a unique ID that will be +same in both sides. In this example, lets set the id as `ext_x_y` where `ext` is your extension id to prevent +name collision with other extensions. Phoenix core reserves the prefix `ph_` for internal use. + +### Create `NodeConnector` in Phoenix side `x.js` + +```js +const NodeConnector = require('NodeConnector'); +const XY_NODE_CONNECTOR_ID = 'ext_x_y'; +let nodeConnector; +const nodeConnectedPromise = NodeConnector.createNodeConnector(XY_NODE_CONNECTOR_ID, exports).then(connector=>{ + nodeConnector = connector; +}); + +exports.modifyImage = async functions(imageName, imageArrayBugger){ + // do some image ops with the imageArrayBugger + // to return an arry buffer, you should return an object that contains a key `buffer` with the `ArrayBuffer` contents. + return { + operationDone: "colored,cropped", + buffer: imageArrayBugger + }; +}; +``` + +### Create `NodeConnector` in Node `y.js` + +```js +const XY_NODE_CONNECTOR_ID = 'ext_x_y'; +let nodeConnector; +const nodeConnectedPromise = global.createNodeConnector(XY_NODE_CONNECTOR_ID, exports).then(connector=>{ + nodeConnector = connector; +}); + +exports.getPWDRelative = async functions(subPath){ + return process.cwd + "/" + subPath; +}; +``` + +After the above, a node connector is now setup and available to use for 2 way communication. + +## Executing Functions in Node from Phcode. + +Suppose that you need to execute a function `getPWDRelative` in node module `y` from Phoenix. +Note that functions that are called with `execPeer` must be async and only takes a single argument. +A second optional argument can be passed which should be an ArrayBuffer to transfer large binary data(See below.). + +### Calling `y.getPWDRelative` node module function from phoenix module `x.js` + +```js +// in x.js +// treat it like just any other function call. The internal RPC bits are handled by the NodeConnector. +// This makes working with node APIs very eazy in phcode. +// ensure that `nodeConnector` is set before using it here as it is returned by a promise! +await nodeConnectedPromise; +const fullPath = await nodeConnector.execPeer('getPWDRelative', "sub/path.html"); +``` + +## Executing Functions in Phcode from Node and sending binary data. + +`execPeer` API accepts a single optional binary data that can be passed to the other side. In this example, we +will transfer an ArrayBuffer from node to phcode function `modifyImage` in module `x.js` + +### Calling `x.modifyImage` phoenix module function from node module `y.js` + +```js +// in y.js +// ensure that `nodeConnector` is set before using it here as it is returned by a promise! +await nodeConnectedPromise; +const {operationDone, buffer} = await nodeConnector.execPeer('modifyImage', "theHills.png", imageAsArrayBuffer); +``` + +## Events - Listening to and raising events between node and phoenix `NodeConnector`. + +The nodeConnector object is an `EventDispatcher` implementing all the apis supported by `utils.EventDispatcher` API. +You can use `nodeConnector.triggerPeer(eventName, data, optionalArrayBuffer)` API to trigger an event on the other side. + +### using `nodeConnector.triggerPeer(eventName, data, optionalArrayBuffer)` + +Lets listen to a named `phoenixProjectOpened` event in node that will be raised from phoenix. + +In `y.js` in node + +```js +// in y.js +// ensure that `nodeConnector` is set before using it here as it is returned by a promise! +nodeConnector.on('phoenixProjectOpened', (_event, projectPath)={ + console.log(projectPath); +}); +``` + +Now in Phoenix module `x.js`, we can raise the event on node by using the `triggerPeer` API. + +```js +// in x.js +nodeConnector.triggerPeer("phoenixProjectOpened", "/x/project/folder"); +// this will now trigger the event in node. +``` + +To listen, unlisten and see more operations of event handling available in `nodeConnector`, see docs for +`utils/EventDispatcher` module. + +### Sending binary data in events + +You can optionally send binary data with `triggerPeer`. See Eg. Below. + +```js +nodeConnector.triggerPeer("imageEdited", "name.png", imageArrayBuffer); +``` + +## createNodeConnector + +Creates a new node connector with the specified ID and module exports. + +Returns a promise that resolves to an NodeConnector Object (which is an EventDispatcher with +additional `execPeer` and `triggerPeer` methods. `peer` here means, if you are executing `execPeer` +in Phoenix, it will execute the named function in node side, and vice versa. +The promise will be resolved only after a call to `createNodeConnector` on the other side with the +same `nodeConnectorID` is made. This is so that once the promise is resolved, you can right away start +two-way communication (exec function, send/receive events) with the other side. + +* execPeer: A function that executes a peer function with specified parameters. +* triggerPeer: A function that triggers an event to be sent to a peer. +* Also contains all the APIs supported by `utils/EventDispatcher` module. + +### Parameters + +* `nodeConnectorID` **[string][1]** The unique identifier for the new node connector. +* `moduleExports` **[Object][2]** The exports of the module that contains the functions to be executed on the other side. + + + +* Throws **[Error][3]** If a node connector with the same ID already exists. + +Returns **[Promise][4]** A promise that resolves to an NodeConnector Object. + +[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String + +[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object + +[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error + +[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise diff --git a/docs/generatedApiDocs/utils/EventDispatcher-API.md b/docs/generatedApiDocs/utils/EventDispatcher-API.md index 7c70515443..0bb4ab38c1 100644 --- a/docs/generatedApiDocs/utils/EventDispatcher-API.md +++ b/docs/generatedApiDocs/utils/EventDispatcher-API.md @@ -2,7 +2,7 @@ ## utils/EventDispatcher -Implements a jQuery-like event dispatch pattern for non-DOM objects (works in web workers as well): +Implements a jQuery-like event dispatch pattern for non-DOM objects (works in web workers and phoenix node as well): * Listeners are attached via on()/one() & detached via off() * Listeners can use namespaces for easy removal @@ -43,11 +43,12 @@ const EventDispatcher = brackets.getModule("utils/EventDispatcher"); ### Using the global object -The EventDispatcher Object is available within the global context, be it phoenix or phoenix core web workers. +The EventDispatcher Object is available within the global context, be it phoenix or phoenix core web workers or node. ```js -window.EventDispatcher.trigger("someEvent"); // within phoenix -self.EventDispatcher.trigger("someEvent"); // within web worker +window.EventDispatcher.makeEventDispatcher(exports); // within phoenix require module +self.EventDispatcher.makeEventDispatcher(object); // within web worker +global.EventDispatcher.makeEventDispatcher(exports); // within node module that has an export ``` If you wish to import event dispatcher to your custom web worker, use the following @@ -55,7 +56,9 @@ If you wish to import event dispatcher to your custom web worker, use the follow ```js importScripts('/utils/EventDispatcher'); // this will add the global EventDispatcher to your web-worker. Note that the EventDispatcher in the web worker -// is a separate domain and cannot raise or listen to events in phoenix/other workers +// and node is a separate domain and cannot raise or listen to events in phoenix/other workers. For triggering events +// between different domains like between node and phcode, see `nodeConnector.triggerPeer` or +// `WorkerComm.triggerPeer` API for communication between phcode and web workers. self.EventDispatcher.trigger("someEvent"); // within web worker ``` diff --git a/src-node/node-connector.js b/src-node/node-connector.js index 003edf0ba2..d580735233 100644 --- a/src-node/node-connector.js +++ b/src-node/node-connector.js @@ -403,6 +403,9 @@ async function createNodeConnector(nodeConnectorID, moduleExports) { if ((dataBuffer && !(dataBuffer instanceof ArrayBuffer)) || dataObjectToSend instanceof ArrayBuffer) { throw new Error("execPeer should be called with exactly 3 or less arguments (FnName:string, data:Object|string, buffer:ArrayBuffer)"); } + if (dataBuffer instanceof ArrayBuffer && !_isObject(dataObjectToSend)) { + throw new Error("execPeer second argument should be an object if sending binary data (FnName:string, data:Object, buffer:ArrayBuffer)"); + } return new Promise((resolve, reject) =>{ currentCommandID ++; pendingExecPromiseMap[currentCommandID] = {resolve, reject}; diff --git a/src-node/test-connection.js b/src-node/test-connection.js index c2b5b6eadc..5307eb0859 100644 --- a/src-node/test-connection.js +++ b/src-node/test-connection.js @@ -151,4 +151,8 @@ async function _shouldErrorOut(a,b) { exports.testErrExecCases = async function () { await _shouldErrorOut(toArrayBuffer("g")); await _shouldErrorOut({}, 34); + let buffer = toArrayBuffer("Hello, World!"); + await _shouldErrorOut("", buffer); + await _shouldErrorOut(34, buffer); + await _shouldErrorOut(null, buffer); }; diff --git a/src/NodeConnector.js b/src/NodeConnector.js new file mode 100644 index 0000000000..d8c1a40b71 --- /dev/null +++ b/src/NodeConnector.js @@ -0,0 +1,161 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +// @INCLUDE_IN_API_DOCS + +/** + * Use this module to communicate easily with node and vice versa. A `NodeConnector` is an intermediary between a + * module in node and a module in phcode. With a `NodeConnector` interface, you can execute a function in node + * from phoenix by simply calling `await nodeConnectorObject.execPeer("namedFunctionInNodeModule, argObject")` and + * the resolved promise will have the result. No need to do any complex IPC or anything else. See a step by step + * example below on how this is done. + * + * ## The setup + * Assume that you have a module in phcode `x.js` and another module in node `y.js`. To communicate between `x` and `y` + * we have to first create a `NodeConnector` on both sides. A `NodeConnector` will have a unique ID that will be + * same in both sides. In this example, lets set the id as `ext_x_y` where `ext` is your extension id to prevent + * name collision with other extensions. Phoenix core reserves the prefix `ph_` for internal use. + * + * ### Create `NodeConnector` in Phoenix side `x.js` + * ```js + * const NodeConnector = require('NodeConnector'); + * const XY_NODE_CONNECTOR_ID = 'ext_x_y'; + * let nodeConnector; + * const nodeConnectedPromise = NodeConnector.createNodeConnector(XY_NODE_CONNECTOR_ID, exports).then(connector=>{ + * nodeConnector = connector; + * }); + * + * exports.modifyImage = async functions(imageName, imageArrayBugger){ + * // do some image ops with the imageArrayBugger + * // to return an arry buffer, you should return an object that contains a key `buffer` with the `ArrayBuffer` contents. + * return { + * operationDone: "colored,cropped", + * buffer: imageArrayBugger + * }; + * }; + * ``` + * + * ### Create `NodeConnector` in Node `y.js` + * ```js + * const XY_NODE_CONNECTOR_ID = 'ext_x_y'; + * let nodeConnector; + * const nodeConnectedPromise = global.createNodeConnector(XY_NODE_CONNECTOR_ID, exports).then(connector=>{ + * nodeConnector = connector; + * }); + * + * exports.getPWDRelative = async functions(subPath){ + * return process.cwd + "/" + subPath; + * }; + * ``` + * + * After the above, a node connector is now setup and available to use for 2 way communication. + * + * ## Executing Functions in Node from Phcode. + * Suppose that you need to execute a function `getPWDRelative` in node module `y` from Phoenix. + * Note that functions that are called with `execPeer` must be async and only takes a single argument. + * A second optional argument can be passed which should be an ArrayBuffer to transfer large binary data(See below.). + * + * ### Calling `y.getPWDRelative` node module function from phoenix module `x.js` + * ```js + * // in x.js + * // treat it like just any other function call. The internal RPC bits are handled by the NodeConnector. + * // This makes working with node APIs very eazy in phcode. + * // ensure that `nodeConnector` is set before using it here as it is returned by a promise! + * await nodeConnectedPromise; + * const fullPath = await nodeConnector.execPeer('getPWDRelative', "sub/path.html"); + * ``` + * + * ## Executing Functions in Phcode from Node and sending binary data. + * `execPeer` API accepts a single optional binary data that can be passed to the other side. In this example, we + * will transfer an ArrayBuffer from node to phcode function `modifyImage` in module `x.js` + * + * ### Calling `x.modifyImage` phoenix module function from node module `y.js` + * ```js + * // in y.js + * // ensure that `nodeConnector` is set before using it here as it is returned by a promise! + * await nodeConnectedPromise; + * const {operationDone, buffer} = await nodeConnector.execPeer('modifyImage', "theHills.png", imageAsArrayBuffer); + * ``` + * + * ## Events - Listening to and raising events between node and phoenix `NodeConnector`. + * The nodeConnector object is an `EventDispatcher` implementing all the apis supported by `utils.EventDispatcher` API. + * You can use `nodeConnector.triggerPeer(eventName, data, optionalArrayBuffer)` API to trigger an event on the other side. + * + * ### using `nodeConnector.triggerPeer(eventName, data, optionalArrayBuffer)` + * Lets listen to a named `phoenixProjectOpened` event in node that will be raised from phoenix. + * + * In `y.js` in node + * ```js + * // in y.js + * // ensure that `nodeConnector` is set before using it here as it is returned by a promise! + * nodeConnector.on('phoenixProjectOpened', (_event, projectPath)={ + * console.log(projectPath); + * }); + * ``` + * + * Now in Phoenix module `x.js`, we can raise the event on node by using the `triggerPeer` API. + * + * ```js + * // in x.js + * nodeConnector.triggerPeer("phoenixProjectOpened", "/x/project/folder"); + * // this will now trigger the event in node. + * ``` + * + * To listen, unlisten and see more operations of event handling available in `nodeConnector`, see docs for + * `utils/EventDispatcher` module. + * + * ### Sending binary data in events + * You can optionally send binary data with `triggerPeer`. See Eg. Below. + * ```js + * nodeConnector.triggerPeer("imageEdited", "name.png", imageArrayBuffer); + * ``` + * + * @module NodeConnector + */ + +define(function (require, exports, module) { + /** + * Creates a new node connector with the specified ID and module exports. + * + * Returns a promise that resolves to an NodeConnector Object (which is an EventDispatcher with + * additional `execPeer` and `triggerPeer` methods. `peer` here means, if you are executing `execPeer` + * in Phoenix, it will execute the named function in node side, and vice versa. + * The promise will be resolved only after a call to `createNodeConnector` on the other side with the + * same `nodeConnectorID` is made. This is so that once the promise is resolved, you can right away start + * two-way communication (exec function, send/receive events) with the other side. + * + * - execPeer: A function that executes a peer function with specified parameters. + * - triggerPeer: A function that triggers an event to be sent to a peer. + * - Also contains all the APIs supported by `utils/EventDispatcher` module. + * + * @param {string} nodeConnectorID - The unique identifier for the new node connector. + * @param {Object} moduleExports - The exports of the module that contains the functions to be executed on the other side. + * + * @returns {Promise} - A promise that resolves to an NodeConnector Object. + * + * @throws {Error} - If a node connector with the same ID already exists. + */ + function createNodeConnector(nodeConnectorID, moduleExports) { + return window.PhNodeEngine.createNodeConnector(nodeConnectorID, moduleExports); + } + + exports.createNodeConnector = createNodeConnector; +}); diff --git a/src/brackets.js b/src/brackets.js index ed475be827..24d039e99e 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -64,6 +64,7 @@ define(function (require, exports, module) { require("utils/EventDispatcher"); require("worker/WorkerComm"); require("utils/ZipUtils"); + require("NodeConnector"); // Load dependent modules const AppInit = require("utils/AppInit"), diff --git a/src/node-loader.js b/src/node-loader.js index 65c87a55fb..7e96f71c8b 100644 --- a/src/node-loader.js +++ b/src/node-loader.js @@ -358,6 +358,9 @@ function nodeLoader() { if ((dataBuffer && !(dataBuffer instanceof ArrayBuffer)) || dataObjectToSend instanceof ArrayBuffer) { throw new Error("execPeer should be called with exactly 3 arguments or less (FnName:string, data:Object|string, buffer:ArrayBuffer)"); } + if (dataBuffer instanceof ArrayBuffer && !_isObject(dataObjectToSend)) { + throw new Error("execPeer second argument should be an object if sending binary data (FnName:string, data:Object, buffer:ArrayBuffer)"); + } return new Promise((resolve, reject) =>{ currentCommandID ++; pendingExecPromiseMap[currentCommandID] = {resolve, reject}; diff --git a/test/SpecRunner.js b/test/SpecRunner.js index 6d8162536a..e1f697bab0 100644 --- a/test/SpecRunner.js +++ b/test/SpecRunner.js @@ -248,6 +248,9 @@ define(function (require, exports, module) { require("features/ParameterHintsManager"); require("features/JumpToDefManager"); + //node connector + require("NodeConnector"); + var selectedCategories, params = new UrlParams(), reporter, diff --git a/test/spec/NodeConnection-test.js b/test/spec/NodeConnection-test.js index 3dc0fdb09b..14a7ca2dc5 100644 --- a/test/spec/NodeConnection-test.js +++ b/test/spec/NodeConnection-test.js @@ -28,6 +28,8 @@ define(function (require, exports, module) { return; } + const NodeConnector = require("NodeConnector"); + function toArrayBuffer(text) { const textEncoder = new TextEncoder(); const uint8Array = textEncoder.encode(text); @@ -62,7 +64,7 @@ define(function (require, exports, module) { }); beforeAll(async function () { - nodeConnector = await window.PhNodeEngine.createNodeConnector(TEST_NODE_CONNECTOR_ID, exports); + nodeConnector = await NodeConnector.createNodeConnector(TEST_NODE_CONNECTOR_ID, exports); exports.echoTestPhcode = function (data, buffer) { console.log("Node fn called testFnCall"); return new Promise(resolve =>{ @@ -150,6 +152,13 @@ define(function (require, exports, module) { await shouldErrorOut({}, 34); }); + it("Should execPeer error out if object is not second parameter while sending binary data", async function () { + let buffer = toArrayBuffer("Hello, World!"); + await shouldErrorOut("", buffer); + await shouldErrorOut(34, buffer); + await shouldErrorOut(null, buffer); + }); + it("Should fail if the connector is given invalid params in node side", async function () { await nodeConnector.execPeer("testErrExecCases"); });