npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hal313/context-portal

v1.0.8

Published

RPC JavaScript Library

Downloads

7

Readme

context-portal

Build Status Dependency Status DevDependency Status npm Version Gitpod ready-to-code

Allows JavaScript to be run in remote or otherwise isolated contexts.

A great use for this is to execute code across boundaries of Chrome extension runtime contexts (the content page and the popup context, for example). As well, this could be used to exceute code across frame boundaries.

Note that the Chrome Extension API does provide a way to execute code across contexts, however there some significant advantages to using this library:

  1. The Chrome Extension API does not handle remote code which executes promises
  2. This library allows functions to be defined in a code context (opposed to a string context), so the code can be evaluated by any toolchain
  3. The Chrome Extension API has some restrictions and incongruent API calls (depending on the type of extension)
// Create and start a portal instance where the code should be run
const windowPortal = new Portal(
    // This function sends messages from the portal to the client
    // NOTE: The sendFunction actually takes two parameters: the message to send AND the orginal request from the remote
    message => window.postMessage(message),
    // This function directs messages sent from the client to the portal message handler
    handler => addEventListener('message', message => handler(message.data))
).start();


// Create an instance of the remote where the actual functions are
const remote = new Remote(
    // This function sends messages from the client to the portal
    message => window.postMessage(message),
    // This function directs messages sent from the portal to the client message handler
    handler => addEventListener('message', message => handler(message.data))
);

// Create an API in the remote context; this creates the same API in the portal context
//
// The return value of this call is an object with all the same functions, however each
// function will return a promise. When invoked, the API will inform the portal context
// to execute the function and will return a promise which resolves to the value returned
// in the portal execution context
remote.createAPI({
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
})
.then(api => {
    return api.add(5, 5)
    .then(result => api.subtract(result/*10*/, 1))
    .then(result => api.multiply(result/*9*/, 10))
    .then(result => api.divide(result/*90*/, 9))
})
.then(result => console.log(`result should be '10': ${10 === result} (${result})`))
.catch(error => console.error('error', error));

It is noteworthy that there are no runtime depedencies required for this library.

Practical Applications

Named Portal and Remote

This example demonstrates how to have multiple portal and remote instances live in the same space. By adding some filters on incoming messages and appending destination data to outgoing messages, any number of portal and remote instances may exist in the same space.

// Create a portal that only listens to messages which have a member "target" with value "pizza"
// Since there are multiple remotes, the target is attached to outgoing messages as well so that
// remotes may ignore messages not intended for them
const pizzaPortal = new Portal(
    // In this scenario, "request" represents the message sent from the remote; the message contains
    // the target ('pizza'); it is OK to use either the string literal or `request.target`
    (message, request) => window.postMessage(Object.assign({}, message, {target: request.target/*pizza*/})),
    handler => addEventListener('message', message => 'pizza' === message.data.target ? handler(message.data) : null)
);
// Start the portal
pizzaPortal.start();
//
// Create a remote which appends the "target" member to outgoing messages with the value "pizza"
// Since there are multiple remote instances, filter out messages not intended for this instance
const pizzaRemote = new Remote(
    message => window.postMessage(Object.assign({}, message, {target: 'pizza'})),
    handler => addEventListener('message', message => 'pizza' === message.data.target ? handler(message.data) : null)
);



// Create a portal that only listens to messages which have a member "target" with value "darko"
// Since there are multiple remotes, the target is attached to outgoing messages as well so that
// remotes may ignore messages not intended for them
const darkoPortal = new Portal(
    // Contrast with pizzaPortal, the target is a string literal; either approach is OK
    message => window.postMessage(Object.assign({}, message, {target: 'darko'})),
    // Filter out messages not intended for this portal
    handler => addEventListener('message', message => 'darko' === message.data.target ? handler(message.data) : null)
);
// Start the portal
darkoPortal.start();
//
// Create a remote which appends the "target" member to outgoing messages with the value "pizza"
// Since there are multiple remote instances, filter out messages not intended for this instance
const darkoRemote = new Remote(
    message => window.postMessage(Object.assign({}, message, {target: 'darko'})),
    // Filter out messages not intended for this portal
    handler => addEventListener('message', message => 'darko' === message.data.target ? handler(message.data) : null)
);


// Run a script only on the pizzaRemote
await pizzaRemote.runScript(`console.log('pizza!')`);
// Run a script only on the darkoRemote
await darkoRemote.runScript(`console.log('donnie!')`);

Chrome Extension

This example demonstrates how a Chrome Extension might use this library in order to execute functions on the content page context from within the popup context. See the full source code.

This code would be executed in the content page context:

// Content script does not run as a module; cannot use async - use promises instead
import('./portal.js').then(Portal => new Portal.Portal(
    // This function sends a message from the portal context (content script) to the remote (popup context)
    chrome.runtime.sendMessage,
    // Register the handler
    handler => chrome.runtime.onMessage.addListener(handler)
).start());

Likewise, the popup context runs this code:

// Instantiate the portal
const remote = new Remote(
  // This function sends a message from the popup context to the portal context (content script - the c urrent tab)
  message => chrome.tabs.query({active: true, currentWindow: true}, tabs => chrome.tabs.sendMessage(tabs[0].id, message)),
  // Register the handler
  handler => chrome.runtime.onMessage.addListener(handler)
);

// Just like before, the API can be created and used; in this case, the actual code is executed in the portal context (content page)
remote.createAPI({
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
})
.then(api => {
    return api.add(5, 5)
    .then(result => api.subtract(result, 1))
    .then(result => api.multiply(result, 10))
    .then(result => api.divide(result, 9))
})
.then(result => console.log(`result should be '10': ${10 === result} (${result})`))
.catch(error => console.error('error', error));

General Use Notes

Parameter Inputs

In general, primative values, arrays and JSON-like objects may be used as parameters and values as API function parameters; as well, Promise's which resolve those types may be used (any parameter which contains Promise's will be resolved within the remote instance before being sent to the portal).

Function Outputs

Return values for API functions may be primative values, arrays, JSON-like objects and also Promises. The promise will be resolved in the portal context before the resolved value is sent to the remote client.

If a function execution throws or contains a result which contains a Promise that rejects, the entire function call is rejected and the remote instance will receive the rejection.

Errors

If execution fails within the portal context, then an error will be received by any remote instances as a Promise rejection. All errors in the remote contain a "message" attribute which indicates the error. If the execution in the portal was an Error instance, an additional "name" attribute will be attached to the error object in the remote. In this case, an actual Error instance will be re-created, however only the message will persist; that is to say, the stack trace from the portal will not be present on the remote.

Limitations

Currently observables and callbacks are not implemented. More precisely, with the exception of the Promise class, no functions which return asynchronous results should be expected to work.

Context and global variables are not implemented:

// Instantiate the portal
const remote = new Remote(
    message => window.postMessage(message),
    handler => addEventListener('message', message => handler(message.data))
);

const helloString = 'Hello';

// Create the API
remote.createAPI({
    // Note that 'helloString' is defined outside the context of this function; this will
    // fail at runtime because the portal context does not know what 'helloString' is
    hello: (name) => helloString + ' ' + name
})
.then(api => api.hello('Pat'))
.then(string => console.log(string))
.catch(error => console.error('error', error));

It is possible to send global variables to the portal context like so:

// Instantiate the portal
const remote = new Remote(
    message => window.postMessage(message),
    handler => addEventListener('message', message => handler(message.data))
);

// Set the constant "helloString"
const helloString = 'Hello';

// Set the variable
remote.runScript(`var helloString = '${helloString}'`)
// Create the API
.then(() => remote.createAPI({
    // Note that 'helloString' is defined outside the context of this function; this will
    // fail at runtime because the portal context does not know what 'helloString' is
    hello: (name) => helloString + ' ' + name
}))
.then(api => api.hello('Pat'))
.then(string => console.log(string))
.catch(error => console.error('error', error));

Architecture

The Portal instance resides in the target context while the Remote instance exists in some other context. As long as a way exists to send and receive messages between the two contexts, then this context portal can be used. The actual means to send and receive messages must be provided by the respective contexts and the context portal handles the serialization of functions, parameters and results. All cross-context communication is asynchronous and therefore context portal handles message transfer by assigning callbackId's for each message. Typical client code need not be concerned with callbackIds, assigning requests to responses and the like.

The remote creates an API to be used. Each function in the API is a wrapper function which will invoke the function on the portal context, while returning a promise. The request includes a callbackId which is stored as a key in the callbackMap within the remote instance (the values are Deferred instances). Once the portal responds with a message, the callbackMap is consulted and the Deferred is either rejected or resolved using the success and payload value of the message.

Message Format

The message formats between the portal and remote instances are documented below but should be of interest only to developers on the project.

portal -> remote

    {

        source: string,     // The source is always "portal"
        action: string,     // Identifies the message action
        payload: Object,    // Response from the code execution
        callbackId: string, // The callback ID (assigned by the remote request)
        success: boolean    // True, if the action was a success
    }

remote -> portal

    {
        source: string,     // The source is always "remote"
        action: string,     // Identifies the message action
        payload: Object,    // Parameters and such for the action
        callbackId: string  // A unique ID for the message (the portal's return message will have the same id)
    };

Developing

Examples

All examples can be run locally, or by visting the hosted examples page.

Playground

A basic HTML page which loads the Portal and Remote classes and can be served through some IDE's, or via the command:

npx http-server -o examples/playground/playground.html

The web IDE has some sample code which can be run in order to see how the Portal and Remote work together. Notice how the both the Portal and Remote reside in the same context. In this case, messages may be passed using window.postMessage.

Frames

A basic HTML page which loads two frames, one for the Portal and one for the Remote. This example can be served through some IDE's, or via the command:

npx http-server -o examples/frames/frames.html

The web IDE has some sample code which can be run in order to see how the Portal and Remote work together. Because the Portal and Remote exist in different contexts, the messages are passed using window.parent.frames[0] and window.parent.frames[1].

Windows

A basic HTML page which loads two windows, one for the Portal and one for the Remote. This example can be served through some IDE's, or via the command:

npx http-server -o examples/windows/window-parent-portal.html

The web IDE has some sample code which can be run in order to see how the Portal and Remote work together. Because the Portal and Remote exist in different windows, the messages are passed using window.childWindow and window.opener.

Tests

Browser

Tests can be run in a browser a few different ways. However, tests MUST be run from a server and not loaded from disk, as doing so will violate security.

It is best to use IDE live-server functionality, as this often includes refreshing the page when code changes.

This package has a built in server, which can be started like:

npm run serve-test

Open a browser to test the Remote: http://127.0.0.1:3000/test/specs/remote

Open a browser to test the Portal: http://127.0.0.1:3000/test/specs/portal

Headless

Unit tests are implemented in Mocha/Chai and can be run within a browser or headless (useful for CI). To run the tests headless:

npm test