Skip to content

Commit

Permalink
Merge af8e4e2 into b1b6b51
Browse files Browse the repository at this point in the history
  • Loading branch information
tbranyen committed Apr 25, 2022
2 parents b1b6b51 + af8e4e2 commit a08cf37
Show file tree
Hide file tree
Showing 21 changed files with 17,472 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/diffhtml-components/lib/render-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export default function renderComponent(vTree, transaction) {
*/
render(props, state) {
return createTree(RawComponent(props, state));
//return createTree(vTree.rawNodeName(props, state));
}

/** @type {VTree | null} */
Expand Down
3 changes: 3 additions & 0 deletions packages/diffhtml-middleware-worker/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["diffhtml-imports"]
}
1 change: 1 addition & 0 deletions packages/diffhtml-middleware-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
81 changes: 81 additions & 0 deletions packages/diffhtml-middleware-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# <±/> diffHTML Worker Middleware

Stable Version: 1.0.0-beta.23

Used to separate the diffing and patching stages of rendering across threads.
The main thread receives payloads containing mutations to apply, all UI logic
is isolated within a worker thread.

Can be used with Node.js servers and pass patches to the client using Web
Sockets.

## Installation

``` sh
npm install diffhtml-middleware-worker
```

## Usage

When you create a worker, you bind it to a mount point and all `innerHTML` or
`outerHTML` calls that get made in the worker are intercepted and passed to the
main thread.

### In a browser

To create a web worker in the browser, import the `createWebWorker` function.
Invoke this with a file path pointing to the location of your module that
contains the rendering logic. This file path will be explained more after the
following code snippet.

You must import either the core or lite version in the main thread to do the
actual patching. The lite version is preferable due to the smaller filesize.
Then register the middleware into the main thread.

```js
import { use } from 'https://diffhtml.org/core/lite';
import { mainTask, createWebWorker } from 'https://diffhtml.org/middleware-worker';

use(mainTask());
```

Once the middleware is registered, you can create web workers using the helper
function. If you already have a worker you can pass it instead of a string to
have the events hooked up.

```js
const mount = document.body;
createWebWorker('./path/to/worker.js', { mount });
```

The above code will create a worker that exists at `worker.js` and proxy all
rendering from it into the mount point, in this case the `<body>` tag. You
must register the `workerTask` middleware inside the worker to ensure patches
are properly passed to the main thread.

```js
import { use, html, createTree, innerHTML } from 'https://diffhtml.org/core';
import { workerTask } from 'https://diffhtml.org/middleware-worker';

use(workerTask());
```

In the worker you must create a placeholder to render into. This will
emulate the mount in the main thread.

```js
// Create an empty fragment to render into, this represents the body tag
// in the main thread.
const placeholder = createTree();

// Any outerHTML or innerHTML calls will be proxied to the main thread mount
// point.
innerHTML(placeholder, html`
<h1>Hello world from a worker thread!</h1>
`);
```

### With Node.js



1 change: 1 addition & 0 deletions packages/diffhtml-middleware-worker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/es/index.js';
14 changes: 14 additions & 0 deletions packages/diffhtml-middleware-worker/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"strict": true,
"module": "esnext",
"target": "es2017",
"declaration": true,
"checkJs": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"outDir": "./dist/typings",
"lib": ["es2017", "dom"]
},
"include": ["lib/**/*"]
}
60 changes: 60 additions & 0 deletions packages/diffhtml-middleware-worker/lib/create-node-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { type } from './util/types';

let Worker = null;

if (typeof global !== 'undefined') {
Worker = (await import('worker_threads')).Worker;
}

const { stringify } = JSON;

/**
*
* @param {string} workerPath
* @param {Object} options
* @param {any} options.socket
* @param {Object} options.workerOpts
* @returns {Worker}
*/
export const createNodeWorker = (workerPath, { socket, workerOpts }) => {
const worker = new Worker(workerPath, { ...(workerOpts || {}) });

/**
* @type {string} msg
*/
const onSocketMessage = (msg) => {
worker.postMessage(msg);
};

worker.on('message', data => {
socket.send(stringify(data));
})
.on('error', (error) => {
console.error(error);

socket.send(stringify({
type: 'log',
level: 'error',
message: String(error.stack),
}));

socket.send(stringify({
type: 'clear',
}));

return true;
})
.on('exit', () => {
socket.off('message', onSocketMessage);

setTimeout(() => {
createNodeWorker(workerPath, { socket, workerOpts });
}, 2000);
});

// Handle incoming messages, must be on main thread to support synchronous
// worker calls.
socket.on('message', onSocketMessage);

return worker;
};
30 changes: 30 additions & 0 deletions packages/diffhtml-middleware-worker/lib/create-web-socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import diff from './util/binding';

const { innerHTML, html } = diff;
const { parse } = JSON;

export const createWebSocket = (socketUrl, { mount, socketOptions }) => {
const socket = new WebSocket(socketUrl, socketOptions);

socket.addEventListener('message', async e => {
const { type, ...rest } = parse(e.data);

// TODO Deprecate the 'clear' event. This is currently used in the Node
// worker when an error is encountered. This cleans up the markup to avoid
// issues during rendering.
if (type === 'clear') {
mount.innerHTML = '';
}

if (type === 'patches') {
innerHTML(mount, null, { patches: rest.patches });
}

if (type === 'log') {
const { level, message } = rest;
console[level](...[].concat(message));
}
});

return socket;
};
37 changes: 37 additions & 0 deletions packages/diffhtml-middleware-worker/lib/create-web-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import diff from './util/binding';
import { type, Mount } from './util/types';

const { createTree, innerHTML } = diff;
const { stringify } = JSON;

/**
* Provides a Web Worker implementation
*
* @param {string} path - Location of worker script
* @param {Object} options
* @param {Mount} options.mount - DOM Node or VTree to render worker contents into
* @param {Object} workerOpts - Supply additional options to new Worker()
* @returns {Worker}
*/
export const createWebWorker = (path, { mount, workerOpts }) => {
const worker = new Worker(path, { type: 'module', ...(workerOpts || {}) });

worker.onmessage = e => {
const { type, ...rest } = e.data;

if (type === 'patches') {
/** @type {any} */
const transactionConfigAsAny = ({ patches: rest.patches });
innerHTML(mount, null, transactionConfigAsAny);
}
};

worker.onerror = e => {
innerHTML(mount, createTree('h1', [
createTree('#text', `Error in worker: ${path}`),
createTree('pre', stringify(e, null, 2)),
]));
};

return worker;
};
17 changes: 17 additions & 0 deletions packages/diffhtml-middleware-worker/lib/get-uuid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import globalThis from './util/global';
import nodeBuffer from './util/node-buffer';

let Blob = globalThis.Blob;

// First attempt to load using Node.js
if (typeof Blob === 'undefined' && nodeBuffer) {
Blob = nodeBuffer.Blob;
}

// If still not available, throw an error.
if (typeof Blob === 'undefined') {
throw new Error('Missing required Blob dependency');
}

// Extract UUID from object URL generated for an arbitrary blob.
export const getUUID = () => URL.createObjectURL(new Blob()).slice(31);
6 changes: 6 additions & 0 deletions packages/diffhtml-middleware-worker/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { getUUID } from './get-uuid';
export { createWebSocket } from './create-web-socket';
export { createWebWorker } from './create-web-worker';
export { createNodeWorker } from './create-node-worker';
export { mainTask } from './main-task';
export { workerTask } from './worker-task';
83 changes: 83 additions & 0 deletions packages/diffhtml-middleware-worker/lib/main-task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import diff from './util/binding';

const { Internals } = diff;

const { assign } = Object;
const { stringify } = JSON;
const linker = new Map();

export const mainTask = ({
// TODO Merge socket and worker args together into one named bridge
// or something similar.
//
// WebSocket connection for the main thread.
socket = null,
worker = null,
} = {}) => assign(function webWorkerTask(transaction) {
const currentTasks = transaction.tasks;
const indexOfSyncTrees = currentTasks.indexOf(Internals.tasks.syncTrees);

// Only run this middleware when patches are present.
if (!('patches' in transaction.config)) {
return;
}

const link = vTree => {
if (vTree && vTree.childNodes) {
Internals.Pool.memory.protected.add(vTree);
linker.set(vTree.__link, vTree);
vTree.childNodes.forEach(x => link(x));
}
};

// Replace syncTrees with injectPatches
currentTasks.splice(indexOfSyncTrees, 1, function injectPatches() {
transaction.patches = transaction.config.patches.map(x => {
// Handle dom events, custom element properties, etc. Any time you need
// a function, we proxy the call and the arguments.
if (x && x.__caller) {
return function(e) {
// TODO handled by synthetic events middleware
e.preventDefault();
e.stopPropagation();

if (socket) {
socket.send(stringify({ type: 'event', ...x }));
}
else {
worker.postMessage({ type: 'event', ...x });
}
};
}

if (!x || typeof x !== 'object' || !('__link' in x)) {
return x;
}

let vTree = x;

if (linker.has(x.__link)) {
vTree = linker.get(x.__link);
return vTree;
}
else if (x.__link === 'mount') {
vTree = transaction.oldTree;
}
else {
link(vTree);
}

if (((x && x.isSvg) || (vTree && vTree.isSvg)) && vTree) {
transaction.state.svgElements.add(vTree);
}

return vTree;
});
});
}, {
releaseHook: vTree => {
if (vTree && vTree.__link) {
linker.delete(vTree.__link);
}
},
});
9 changes: 9 additions & 0 deletions packages/diffhtml-middleware-worker/lib/util/binding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import globalThis from './global';
import { DIFF_BINDING } from './types';

/**
* @returns {DIFF_BINDING}
*/
export default /** @type {DIFF_BINDING} */ (
/** @type {any} */ (globalThis)[Symbol.for('diffHTML')]
);
6 changes: 6 additions & 0 deletions packages/diffhtml-middleware-worker/lib/util/global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {any} */
export default (
typeof global === 'object'
? global
: (typeof window === 'object' ? window : self) || {}
);
8 changes: 8 additions & 0 deletions packages/diffhtml-middleware-worker/lib/util/node-buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let nodeBuffer = null;

try {
nodeBuffer = (await import('buffer')).default;
}
catch {}

export default nodeBuffer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let nodeWorkerThreads = null;

try {
nodeWorkerThreads = (await import('worker_threads')).default;
}
catch {}

export default nodeWorkerThreads;

0 comments on commit a08cf37

Please sign in to comment.