diff --git a/.gitignore b/.gitignore index 4a0d98d6b..dd1ff5e17 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ yarn-error.log yarn.lock package-lock.json demo/build/ +demo/example/ dist/* \ No newline at end of file diff --git a/config/rollup.config.js b/config/rollup.config.js index 35028d153..b1ea7d341 100644 --- a/config/rollup.config.js +++ b/config/rollup.config.js @@ -17,7 +17,4 @@ import MainThreadBuilds from './rollup.main-thread.js'; import WorkerThreadBuilds from './rollup.worker-thread.js'; -export default [ - ...MainThreadBuilds, - ...WorkerThreadBuilds, -]; \ No newline at end of file +export default [...MainThreadBuilds, ...WorkerThreadBuilds]; diff --git a/config/rollup.main-thread.js b/config/rollup.main-thread.js index 1bb39e927..bb010bf7d 100644 --- a/config/rollup.main-thread.js +++ b/config/rollup.main-thread.js @@ -17,8 +17,9 @@ import resolve from 'rollup-plugin-node-resolve'; import compiler from '@ampproject/rollup-plugin-closure-compiler'; import { terser } from 'rollup-plugin-terser'; -import {babelPlugin} from './rollup.plugins.js'; -import {MINIFY_BUNDLE_VALUE, DEBUG_BUNDLE_VALUE} from './rollup.utils.js'; +import replace from 'rollup-plugin-replace'; +import { babelPlugin, removeDebugCommandExecutors } from './rollup.plugins.js'; +import { MINIFY_BUNDLE_VALUE, DEBUG_BUNDLE_VALUE } from './rollup.utils.js'; const ESModules = [ { @@ -29,13 +30,17 @@ const ESModules = [ sourcemap: true, }, plugins: [ + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, }), MINIFY_BUNDLE_VALUE ? compiler() : null, MINIFY_BUNDLE_VALUE ? terser() : null, - ].filter(Boolean) + ].filter(Boolean), }, { input: 'output/main-thread/index.js', @@ -45,11 +50,15 @@ const ESModules = [ sourcemap: true, }, plugins: [ + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, }), - ].filter(Boolean) + ].filter(Boolean), }, { input: 'output/main-thread/index.safe.js', @@ -60,13 +69,17 @@ const ESModules = [ }, plugins: [ resolve(), + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, }), MINIFY_BUNDLE_VALUE ? compiler() : null, MINIFY_BUNDLE_VALUE ? terser() : null, - ].filter(Boolean) + ].filter(Boolean), }, { input: 'output/main-thread/index.safe.js', @@ -77,11 +90,15 @@ const ESModules = [ }, plugins: [ resolve(), + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, }), - ].filter(Boolean) + ].filter(Boolean), }, ]; @@ -95,13 +112,17 @@ const IIFEModules = [ sourcemap: true, }, plugins: [ + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, }), MINIFY_BUNDLE_VALUE ? compiler() : null, MINIFY_BUNDLE_VALUE ? terser() : null, - ].filter(Boolean) + ].filter(Boolean), }, { input: 'output/main-thread/index.js', @@ -112,11 +133,15 @@ const IIFEModules = [ sourcemap: true, }, plugins: [ + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, }), - ].filter(Boolean) + ].filter(Boolean), }, { input: 'output/main-thread/index.safe.js', @@ -128,13 +153,17 @@ const IIFEModules = [ }, plugins: [ resolve(), + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, }), MINIFY_BUNDLE_VALUE ? compiler() : null, MINIFY_BUNDLE_VALUE ? terser() : null, - ].filter(Boolean) + ].filter(Boolean), }, { input: 'output/main-thread/index.safe.js', @@ -146,36 +175,41 @@ const IIFEModules = [ }, plugins: [ resolve(), + removeDebugCommandExecutors(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, }), - ].filter(Boolean) - } + ].filter(Boolean), + }, ]; -const debugModules = DEBUG_BUNDLE_VALUE ? [ - { - input: 'output/main-thread/index.js', - output: { - file: 'dist/debug.index.js', - format: 'iife', - name: 'MainThread', - sourcemap: true, - }, - plugins: [ - babelPlugin({ - transpileToES5: false, - allowConsole: true, - }), - MINIFY_BUNDLE_VALUE ? compiler() : null, - MINIFY_BUNDLE_VALUE ? terser() : null, - ].filter(Boolean) - } -] : []; +const debugModules = DEBUG_BUNDLE_VALUE + ? [ + { + input: 'output/main-thread/index.js', + output: { + file: 'dist/debug.index.js', + format: 'iife', + name: 'MainThread', + sourcemap: true, + }, + plugins: [ + replace({ + DEBUG_ENABLED: true, + }), + babelPlugin({ + transpileToES5: false, + allowConsole: true, + }), + MINIFY_BUNDLE_VALUE ? compiler() : null, + MINIFY_BUNDLE_VALUE ? terser() : null, + ].filter(Boolean), + }, + ] + : []; -export default [ - ...ESModules, - ...IIFEModules, - ...debugModules, -]; \ No newline at end of file +export default [...ESModules, ...IIFEModules, ...debugModules]; diff --git a/config/rollup.plugins.js b/config/rollup.plugins.js index ec0d058da..23c0e2ac8 100644 --- a/config/rollup.plugins.js +++ b/config/rollup.plugins.js @@ -16,16 +16,18 @@ import babel from 'rollup-plugin-babel'; import MagicString from 'magic-string'; +import fs from 'fs'; +import path from 'path'; const walk = require('acorn-walk'); /** * Invoke Babel on source, with some configuration. - * @param {object} config, two keys transpileToES5, and allowConsole + * @param {object} config, two keys transpileToES5, and allowConsole * - transpileToES5 Should we transpile down to ES5 or features supported by `module` capable browsers? * - allowConsole Should we allow `console` methods in the output? * - allowPostMessage Should we allow postMessage to/from the Worker? */ -export function babelPlugin({transpileToES5, allowConsole = false, allowPostMessage = true}) { +export function babelPlugin({ transpileToES5, allowConsole = false, allowPostMessage = true }) { const targets = transpileToES5 ? { browsers: ['last 2 versions', 'ie >= 11', 'safari >= 7'] } : { esmodules: true }; const exclude = allowConsole ? ['error', 'warn', 'trace', 'info', 'log', 'time', 'timeEnd'] : []; @@ -44,19 +46,24 @@ export function babelPlugin({transpileToES5, allowConsole = false, allowPostMess plugins: [ ['@babel/plugin-proposal-object-rest-spread'], ['@babel/proposal-class-properties'], - ['babel-plugin-minify-replace', { - 'replacements': [{ - 'identifierName': '__ALLOW_POST_MESSAGE__', - 'replacement': { - 'type': 'booleanLiteral', - 'value': allowPostMessage - } - }] - }], + [ + 'babel-plugin-minify-replace', + { + replacements: [ + { + identifierName: '__ALLOW_POST_MESSAGE__', + replacement: { + type: 'booleanLiteral', + value: allowPostMessage, + }, + }, + ], + }, + ], ['babel-plugin-transform-remove-console', { exclude }], ], }); -}; +} /** * RollupPlugin that removes the testing document singleton from output source. @@ -69,25 +76,86 @@ export function removeTestingDocument() { buildStart() { context = this; }, - renderChunk: async (code) => { + async renderChunk(code) { const source = new MagicString(code); const program = context.parse(code, { ranges: true }); walk.simple(program, { VariableDeclarator(node) { if (node.id && node.id.type === 'Identifier' && node.id.name && node.id.name === 'documentForTesting') { - const range = node.range; - if (range) { + if (node.range) { source.overwrite(node.range[0], node.range[1], 'documentForTesting = undefined'); } } }, }); - + + return { + code: source.toString(), + map: source.generateMap(), + }; + }, + }; +} + +/** + * Formats valid output for trimmed ObjectExpressions. + * @param {string} code + * @param {Array>} validPropertyRanges + */ +const outputPropertyRange = (code, validPropertyRanges) => + `{ + ${validPropertyRanges.map((range, index) => `${index > 0 ? '\n\t\t' : ''}${code.substring(range[0], range[1])}`)} + }`; + +/** + * RollupPlugin that removes the debugging printers from CommandExecutors. + */ +export function removeDebugCommandExecutors() { + let context; + let toDiscover; + + return { + name: 'remove-debug-command-executors', + buildStart(options) { + context = this; + toDiscover = fs + .readdirSync(path.join(path.dirname(options.input), 'commands')) + .filter(file => path.extname(file) !== '.map' && path.basename(file, '.js') !== 'interface').length; + }, + async renderChunk(code) { + const source = new MagicString(code); + const program = context.parse(code, { ranges: true }); + + walk.simple(program, { + ObjectExpression(node) { + const propertyNames = (node.properties && node.properties.map(property => property.key.name)) || []; + const validPropertyRanges = []; + + if (propertyNames.includes('execute') && propertyNames.includes('print')) { + for (const property of node.properties) { + if (property.key.type === 'Identifier') { + if (property.key.name === 'print') { + toDiscover--; + } else { + validPropertyRanges.push([property.range[0], property.range[1]]); + } + } + } + + source.overwrite(node.range[0], node.range[1], outputPropertyRange(code, validPropertyRanges)); + } + }, + }); + + if (toDiscover > 0) { + context.warn(`${toDiscover} CommandExecutors were not found during compilation.`); + } + return { code: source.toString(), map: source.generateMap(), }; }, }; -} \ No newline at end of file +} diff --git a/config/rollup.utils.js b/config/rollup.utils.js index 61ee53b00..23ad61011 100644 --- a/config/rollup.utils.js +++ b/config/rollup.utils.js @@ -18,4 +18,4 @@ const { DEBUG_BUNDLE = false, MINIFY_BUNDLE = false, COMPRESS_BUNDLE = false } = export let DEBUG_BUNDLE_VALUE = DEBUG_BUNDLE === 'true'; export let MINIFY_BUNDLE_VALUE = MINIFY_BUNDLE === 'true'; -export let COMPRESS_BUNDLE_VALUE = COMPRESS_BUNDLE === 'true'; \ No newline at end of file +export let COMPRESS_BUNDLE_VALUE = COMPRESS_BUNDLE === 'true'; diff --git a/config/rollup.worker-thread.js b/config/rollup.worker-thread.js index 450e6ee57..eb43c72df 100644 --- a/config/rollup.worker-thread.js +++ b/config/rollup.worker-thread.js @@ -16,6 +16,7 @@ import compiler from '@ampproject/rollup-plugin-closure-compiler'; import { terser } from 'rollup-plugin-terser'; +import replace from 'rollup-plugin-replace'; import { babelPlugin, removeTestingDocument } from './rollup.plugins.js'; import { MINIFY_BUNDLE_VALUE, DEBUG_BUNDLE_VALUE } from './rollup.utils.js'; @@ -33,6 +34,9 @@ const ESModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, @@ -55,6 +59,9 @@ const ESModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, @@ -71,6 +78,9 @@ const ESModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, @@ -93,6 +103,9 @@ const ESModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, @@ -112,6 +125,9 @@ const IIFEModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, @@ -134,6 +150,9 @@ const IIFEModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, @@ -150,6 +169,9 @@ const IIFEModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, @@ -172,6 +194,9 @@ const IIFEModules = [ }, plugins: [ removeTestingDocument(), + replace({ + DEBUG_ENABLED: false, + }), babelPlugin({ transpileToES5: true, allowConsole: DEBUG_BUNDLE_VALUE, @@ -192,6 +217,9 @@ const debugModules = DEBUG_BUNDLE_VALUE outro: 'window.workerDocument = documentForTesting;', }, plugins: [ + replace({ + DEBUG_ENABLED: true, + }), babelPlugin({ transpileToES5: false, allowConsole: DEBUG_BUNDLE_VALUE, diff --git a/config/tsconfig.test.json b/config/tsconfig.tests.json similarity index 100% rename from config/tsconfig.test.json rename to config/tsconfig.tests.json diff --git a/demo/long-task/index.html b/demo/long-task/index.html index 6c92142c9..27ac68a70 100644 --- a/demo/long-task/index.html +++ b/demo/long-task/index.html @@ -1,55 +1,62 @@ - - - Hello World - - - - - - - - - - -
-
- + + + Hello World + + + + + + + + + + +
+
+ +
-
-
-
- - + + + + - - - + diff --git a/demo/long-task/long-task-demo.js b/demo/long-task/long-task-demo.js index ab547cbe7..633022135 100644 --- a/demo/long-task/long-task-demo.js +++ b/demo/long-task/long-task-demo.js @@ -16,12 +16,12 @@ const btn = document.getElementsByTagName('button')[0]; -btn.addEventListener('click', () => { +btn.addEventListener('click', _ => { fetch('http://localhost:3001/slow/long-task/data.json') - .then(response => response.json()) - .then(json => { - const h1 = document.createElement('h1'); - h1.textContent = 'Hello ' + json.year + ' World!' - document.body.appendChild(h1); - }); + .then(response => response.json()) + .then(json => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hello ' + json.year + ' World!'; + document.body.appendChild(h1); + }); }); diff --git a/demo/preact-dbmon/index.html b/demo/preact-dbmon/index.html index 694d20eec..5e5b3b21a 100644 --- a/demo/preact-dbmon/index.html +++ b/demo/preact-dbmon/index.html @@ -29,5 +29,6 @@ MainThread.upgradeElement(document.getElementById('upgrade-me'), '/dist/worker.js'); }, false); --> + \ No newline at end of file diff --git a/demo/preact-dbmon/monitor.js b/demo/preact-dbmon/monitor.js new file mode 100644 index 000000000..c73fef548 --- /dev/null +++ b/demo/preact-dbmon/monitor.js @@ -0,0 +1,110 @@ +/** + * @author mrdoob / http://mrdoob.com/ + * @author jetienne / http://jetienne.com/ + * @author paulirish / http://paulirish.com/ + */ +var MemoryStats = function() { + var msMin = 100; + var msMax = 0; + + var container = document.createElement('div'); + container.id = 'stats'; + container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; + + var msDiv = document.createElement('div'); + msDiv.id = 'ms'; + msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; + container.appendChild(msDiv); + + var msText = document.createElement('div'); + msText.id = 'msText'; + msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; + msText.innerHTML = 'Memory'; + msDiv.appendChild(msText); + + var msGraph = document.createElement('div'); + msGraph.id = 'msGraph'; + msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0'; + msDiv.appendChild(msGraph); + + while (msGraph.children.length < 74) { + var bar = document.createElement('span'); + bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131'; + msGraph.appendChild(bar); + } + + var updateGraph = function(dom, height, color) { + var child = dom.appendChild(dom.firstChild); + child.style.height = height + 'px'; + if (color) child.style.backgroundColor = color; + }; + + var perf = window.performance || {}; + // polyfill usedJSHeapSize + if (!perf && !perf.memory) { + perf.memory = { usedJSHeapSize: 0 }; + } + if (perf && !perf.memory) { + perf.memory = { usedJSHeapSize: 0 }; + } + + // support of the API? + if (perf.memory.totalJSHeapSize === 0) { + console.warn('totalJSHeapSize === 0... performance.memory is only available in Chrome .'); + } + + // TODO, add a sanity check to see if values are bucketed. + // If so, reminde user to adopt the --enable-precise-memory-info flag. + // open -a "/Applications/Google Chrome.app" --args --enable-precise-memory-info + + var lastTime = Date.now(); + var lastUsedHeap = perf.memory.usedJSHeapSize; + return { + domElement: container, + + update: function() { + // refresh only 30time per second + if (Date.now() - lastTime < 1000 / 30) return; + lastTime = Date.now(); + + var delta = perf.memory.usedJSHeapSize - lastUsedHeap; + lastUsedHeap = perf.memory.usedJSHeapSize; + var color = delta < 0 ? '#830' : '#131'; + + var ms = perf.memory.usedJSHeapSize; + msMin = Math.min(msMin, ms); + msMax = Math.max(msMax, ms); + msText.textContent = 'Mem: ' + bytesToSize(ms, 2); + + var normValue = ms / (30 * 1024 * 1024); + var height = Math.min(30, 30 - normValue * 30); + updateGraph(msGraph, height, color); + + function bytesToSize(bytes, nFractDigit) { + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes == 0) return 'n/a'; + nFractDigit = nFractDigit !== undefined ? nFractDigit : 0; + var precision = Math.pow(10, nFractDigit); + var i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round((bytes * precision) / Math.pow(1024, i)) / precision + ' ' + sizes[i]; + } + }, + }; +}; + +(function() { + var stats = new MemoryStats(); + stats.domElement.style.position = 'fixed'; + stats.domElement.style.right = '0px'; + stats.domElement.style.bottom = '0px'; + document.body.appendChild(stats.domElement); + requestAnimationFrame(function rAFloop() { + stats.update(); + requestAnimationFrame(rAFloop); + }); + + return { + memoryStats: stats, + renderRate: renderRate, + }; +})(); diff --git a/package.json b/package.json index 59aa4c8f3..8d6b67f5b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "scripts": { "tsc:test:worker": "tsc -p config/tsconfig.test.worker-thread.json", "tsc:test:main": "tsc -p config/tsconfig.test.main-thread.json", - "tsc:test:tests": "tsc -p config/tsconfig.test.json", + "tsc:test:tests": "tsc -p config/tsconfig.tests.json", "tsc:test:tests-main": "tsc -p config/tsconfig.tests.main-thread.json", "tsc:build:worker": "tsc -p config/tsconfig.build.worker-thread.json", "tsc:build:main": "tsc -p config/tsconfig.build.main-thread.json", @@ -67,6 +67,7 @@ "rollup": "1.7.3", "rollup-plugin-babel": "4.3.2", "rollup-plugin-node-resolve": "4.0.1", + "rollup-plugin-replace": "2.1.0", "rollup-plugin-terser": "4.0.4", "serve-static": "1.13.2", "tslint": "5.14.0", @@ -76,6 +77,10 @@ "*.ts": [ "prettier --config config/.prettierrc --write", "git add" + ], + "*.js": [ + "prettier --config config/.prettierrc --write", + "git add" ] }, "husky": { diff --git a/src/main-thread/commands/attribute.ts b/src/main-thread/commands/attribute.ts new file mode 100644 index 000000000..7c08c1267 --- /dev/null +++ b/src/main-thread/commands/attribute.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AttributeMutationIndex } from '../../transfer/TransferrableMutation'; +import { CommandExecutor } from './interface'; +import { Strings } from '../strings'; +import { WorkerDOMConfiguration } from '../configuration'; + +export function AttributeProcessor(strings: Strings, config: WorkerDOMConfiguration): CommandExecutor { + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + const attributeName = strings.get(mutations[startPosition + AttributeMutationIndex.Name]); + // Value is sent as 0 when it's the default value or removal. + // Value is sent as index + 1 when it's a valid value. + const value = + (mutations[startPosition + AttributeMutationIndex.Value] !== 0 && strings.get(mutations[startPosition + AttributeMutationIndex.Value] - 1)) || + null; + + if (attributeName != null) { + if (value == null) { + target.removeAttribute(attributeName); + } else { + if (!config.sanitizer || config.sanitizer.validAttribute(target.nodeName, attributeName, value)) { + target.setAttribute(attributeName, value); + } else { + // TODO(choumx): Inform worker that sanitizer ignored unsafe attribute value change. + } + } + } + return startPosition + AttributeMutationIndex.End; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + const attributeName = strings.get(mutations[startPosition + AttributeMutationIndex.Name]); + // Value is sent as 0 when it's the default value or removal. + // Value is sent as index + 1 when it's a valid value. + const value = + (mutations[startPosition + AttributeMutationIndex.Value] !== 0 && strings.get(mutations[startPosition + AttributeMutationIndex.Value] - 1)) || + null; + + return { + target, + attributeName, + value, + remove: value == null, + }; + }, + }; +} diff --git a/src/main-thread/commands/bounding-client-rect.ts b/src/main-thread/commands/bounding-client-rect.ts index 8c11760ac..cb306af41 100644 --- a/src/main-thread/commands/bounding-client-rect.ts +++ b/src/main-thread/commands/bounding-client-rect.ts @@ -14,54 +14,40 @@ * limitations under the License. */ -import { TransferrableMutationRecord } from '../../transfer/TransferrableRecord'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; import { MessageType } from '../../transfer/Messages'; -import { NodeContext } from '../nodes'; -import { NumericBoolean } from '../../utils'; import { WorkerContext } from '../worker'; +import { CommandExecutor } from './interface'; +import { BoundClientRectMutationIndex } from '../../transfer/TransferrableBoundClientRect'; -export class BoundingClientRectProcessor { - private nodeContext: NodeContext; - private workerContext: WorkerContext; +export function BoundingClientRectProcessor(workerContext: WorkerContext): CommandExecutor { + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + if (target) { + const boundingRect = target.getBoundingClientRect(); + workerContext.messageToWorker({ + [TransferrableKeys.type]: MessageType.GET_BOUNDING_CLIENT_RECT, + [TransferrableKeys.target]: [target._index_], + [TransferrableKeys.data]: [ + boundingRect.top, + boundingRect.right, + boundingRect.bottom, + boundingRect.left, + boundingRect.width, + boundingRect.height, + ], + }); + } else { + console.error(`getNode() yields null – ${target}`); + } - /** - * @param nodeContext - * @param workerContext whom to dispatch events toward. - */ - constructor(nodeContext: NodeContext, workerContext: WorkerContext) { - this.nodeContext = nodeContext; - this.workerContext = workerContext; - } - - /** - * Process commands transfered from worker thread to main thread. - * @param mutation mutation record containing commands to execute. - */ - process(mutation: TransferrableMutationRecord): void { - const nodeId = mutation[TransferrableKeys.target]; - const target = this.nodeContext.getNode(nodeId); - - if (!target) { - console.error('getNode() yields a null value. Node id (' + nodeId + ') was not found.'); - return; - } - - const boundingRect = target.getBoundingClientRect(); - this.workerContext.messageToWorker({ - [TransferrableKeys.type]: MessageType.GET_BOUNDING_CLIENT_RECT, - [TransferrableKeys.target]: { - [TransferrableKeys.index]: target._index_, - [TransferrableKeys.transferred]: NumericBoolean.TRUE, - }, - [TransferrableKeys.data]: [ - boundingRect.top, - boundingRect.right, - boundingRect.bottom, - boundingRect.left, - boundingRect.width, - boundingRect.height, - ], - }); - } + return startPosition + BoundClientRectMutationIndex.End; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + return { + type: 'GET_BOUNDING_CLIENT_RECT', + target, + }; + }, + }; } diff --git a/src/main-thread/commands/character-data.ts b/src/main-thread/commands/character-data.ts new file mode 100644 index 000000000..3ed509021 --- /dev/null +++ b/src/main-thread/commands/character-data.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CharacterDataMutationIndex } from '../../transfer/TransferrableMutation'; +import { CommandExecutor } from './interface'; +import { Strings } from '../strings'; + +export function CharacterDataProcessor(strings: Strings): CommandExecutor { + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + const value = mutations[startPosition + CharacterDataMutationIndex.Value]; + if (value) { + // Sanitization not necessary for textContent. + target.textContent = strings.get(value); + } + return startPosition + CharacterDataMutationIndex.End; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + return { + target, + value: strings.get(mutations[startPosition + CharacterDataMutationIndex.Value]), + }; + }, + }; +} diff --git a/src/main-thread/commands/child-list.ts b/src/main-thread/commands/child-list.ts new file mode 100644 index 000000000..5168ca78c --- /dev/null +++ b/src/main-thread/commands/child-list.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ChildListMutationIndex } from '../../transfer/TransferrableMutation'; +import { CommandExecutor } from './interface'; +import { NodeContext } from '../nodes'; + +export function ChildListProcessor({ getNode }: NodeContext): CommandExecutor { + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + const appendNodeCount = mutations[startPosition + ChildListMutationIndex.AppendedNodeCount]; + const removeNodeCount = mutations[startPosition + ChildListMutationIndex.RemovedNodeCount]; + if (removeNodeCount > 0) { + mutations + .slice( + startPosition + ChildListMutationIndex.Nodes + appendNodeCount, + startPosition + ChildListMutationIndex.Nodes + appendNodeCount + removeNodeCount, + ) + .forEach(removeId => { + const node = getNode(removeId); + if (!node) { + console.error(`getNode() yields null – ${removeId}`); + return; + } + node.remove(); + }); + } + if (appendNodeCount > 0) { + mutations + .slice(startPosition + ChildListMutationIndex.Nodes, startPosition + ChildListMutationIndex.Nodes + appendNodeCount) + .forEach(addId => { + const nextSibling = mutations[startPosition + ChildListMutationIndex.NextSibling]; + const newNode = getNode(addId); + if (newNode) { + // TODO: Handle this case --- + // Transferred nodes that are not stored were previously removed by the sanitizer. + target.insertBefore(newNode, (nextSibling && getNode(nextSibling)) || null); + } + }); + } + return startPosition + ChildListMutationIndex.End + appendNodeCount + removeNodeCount; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + const appendNodeCount = mutations[startPosition + ChildListMutationIndex.AppendedNodeCount]; + const removeNodeCount = mutations[startPosition + ChildListMutationIndex.RemovedNodeCount]; + const removedNodes = Array.from( + mutations.slice( + startPosition + ChildListMutationIndex.Nodes + appendNodeCount, + startPosition + ChildListMutationIndex.Nodes + appendNodeCount + removeNodeCount, + ), + ).map(index => getNode(index) || index); + const addedNodes = Array.from( + mutations.slice(startPosition + ChildListMutationIndex.Nodes, startPosition + ChildListMutationIndex.Nodes + appendNodeCount), + ).map(index => getNode(index) || index); + + return { + target, + nextSibling: getNode(mutations[startPosition + ChildListMutationIndex.NextSibling]) || null, + previousSibling: getNode(mutations[startPosition + ChildListMutationIndex.PreviousSibling]) || null, + addedNodes, + removedNodes, + }; + }, + }; +} diff --git a/src/main-thread/commands/event-subscription.ts b/src/main-thread/commands/event-subscription.ts index 53929b4bc..80b85cb52 100644 --- a/src/main-thread/commands/event-subscription.ts +++ b/src/main-thread/commands/event-subscription.ts @@ -15,78 +15,11 @@ */ import { MessageType } from '../../transfer/Messages'; -import { NumericBoolean } from '../../utils'; -import { NodeContext } from '../nodes'; import { Strings } from '../strings'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { TransferrableMutationRecord } from '../../transfer/TransferrableRecord'; +import { EVENT_SUBSCRIPTION_LENGTH, EventSubscriptionMutationIndex } from '../../transfer/TransferrableEvent'; import { WorkerContext } from '../worker'; - -export class EventSubscriptionProcessor { - private strings: Strings; - private nodeContext: NodeContext; - private workerContext: WorkerContext; - // TODO(choumx): Support SYNC events for properties other than 'value', e.g. 'checked'. - private knownListeners: Array<(event: Event) => any>; - - constructor(strings: Strings, nodeContext: NodeContext, workerContext: WorkerContext) { - this.strings = strings; - this.nodeContext = nodeContext; - this.workerContext = workerContext; - this.knownListeners = []; - } - - /** - * Process event subscription changes transfered from worker thread to main thread. - * @param mutation mutation record containing commands to execute. - */ - process(mutation: TransferrableMutationRecord): void { - const nodeId = mutation[TransferrableKeys.target]; - const target = this.nodeContext.getNode(nodeId); - - if (!target) { - console.error('getNode() yields a null value. Node id (' + nodeId + ') was not found.'); - return; - } - - (mutation[TransferrableKeys.removedEvents] || []).forEach(eventSub => - this.processListenerChange(target, false, this.strings.get(eventSub[TransferrableKeys.type]), eventSub[TransferrableKeys.index]), - ); - (mutation[TransferrableKeys.addedEvents] || []).forEach(eventSub => - this.processListenerChange(target, true, this.strings.get(eventSub[TransferrableKeys.type]), eventSub[TransferrableKeys.index]), - ); - } - - /** - * If the worker requests to add an event listener to 'change' for something the foreground thread is already listening to, - * ensure that only a single 'change' event is attached to prevent sending values multiple times. - * @param target node to change listeners on - * @param addEvent is this an 'addEvent' or 'removeEvent' change - * @param type event type requested to change - * @param index number in the listeners array this event corresponds to. - */ - private processListenerChange(target: RenderableElement, addEvent: boolean, type: string, index: number): void { - let changeEventSubscribed: boolean = target.onchange !== null; - const shouldTrack: boolean = shouldTrackChanges(target as HTMLElement); - const isChangeEvent = type === 'change'; - - if (addEvent) { - if (isChangeEvent) { - changeEventSubscribed = true; - target.onchange = null; - } - (target as HTMLElement).addEventListener(type, (this.knownListeners[index] = eventHandler(this.workerContext, target._index_))); - } else { - if (isChangeEvent) { - changeEventSubscribed = false; - } - (target as HTMLElement).removeEventListener(type, this.knownListeners[index]); - } - if (shouldTrack && !changeEventSubscribed) { - applyDefaultChangeListener(this.workerContext, target as RenderableElement); - } - } -} +import { CommandExecutor } from './interface'; /** * Instead of a whitelist of elements that need their value tracked, use the existence @@ -111,7 +44,7 @@ const applyDefaultChangeListener = (workerContext: WorkerContext, node: Renderab * @param worker whom to dispatch value toward. * @param node where to get the value from. */ -const fireValueChange = (workerContext: WorkerContext, node: RenderableElement): void => { +const fireValueChange = (workerContext: WorkerContext, node: RenderableElement): void => workerContext.messageToWorker({ [TransferrableKeys.type]: MessageType.SYNC, [TransferrableKeys.sync]: { @@ -119,7 +52,6 @@ const fireValueChange = (workerContext: WorkerContext, node: RenderableElement): [TransferrableKeys.value]: node.value, }, }); -}; /** * Register an event handler for dispatching events to worker thread @@ -131,6 +63,7 @@ const eventHandler = (workerContext: WorkerContext, index: number) => (event: Ev if (shouldTrackChanges(event.currentTarget as HTMLElement)) { fireValueChange(workerContext, event.currentTarget as RenderableElement); } + workerContext.messageToWorker({ [TransferrableKeys.type]: MessageType.EVENT, [TransferrableKeys.event]: { @@ -138,21 +71,93 @@ const eventHandler = (workerContext: WorkerContext, index: number) => (event: Ev [TransferrableKeys.bubbles]: event.bubbles, [TransferrableKeys.cancelable]: event.cancelable, [TransferrableKeys.cancelBubble]: event.cancelBubble, - [TransferrableKeys.currentTarget]: { - [TransferrableKeys.index]: (event.currentTarget as RenderableElement)._index_, - [TransferrableKeys.transferred]: NumericBoolean.TRUE, - }, + [TransferrableKeys.currentTarget]: [(event.currentTarget as RenderableElement)._index_], [TransferrableKeys.defaultPrevented]: event.defaultPrevented, [TransferrableKeys.eventPhase]: event.eventPhase, [TransferrableKeys.isTrusted]: event.isTrusted, [TransferrableKeys.returnValue]: event.returnValue, - [TransferrableKeys.target]: { - [TransferrableKeys.index]: (event.target as RenderableElement)._index_, - [TransferrableKeys.transferred]: NumericBoolean.TRUE, - }, + [TransferrableKeys.target]: [(event.target as RenderableElement)._index_], [TransferrableKeys.timeStamp]: event.timeStamp, [TransferrableKeys.type]: event.type, [TransferrableKeys.keyCode]: 'keyCode' in event ? event.keyCode : undefined, }, }); }; + +export function EventSubscriptionProcessor(strings: Strings, workerContext: WorkerContext): CommandExecutor { + const knownListeners: Array<(event: Event) => any> = []; + + /** + * If the worker requests to add an event listener to 'change' for something the foreground thread is already listening to, + * ensure that only a single 'change' event is attached to prevent sending values multiple times. + * @param target node to change listeners on + * @param addEvent is this an 'addEvent' or 'removeEvent' change + * @param type event type requested to change + * @param index number in the listeners array this event corresponds to. + */ + const processListenerChange = (target: RenderableElement, addEvent: boolean, type: string, index: number): void => { + let changeEventSubscribed: boolean = target.onchange !== null; + const shouldTrack: boolean = shouldTrackChanges(target as HTMLElement); + const isChangeEvent = type === 'change'; + + if (addEvent) { + if (isChangeEvent) { + changeEventSubscribed = true; + target.onchange = null; + } + (target as HTMLElement).addEventListener(type, (knownListeners[index] = eventHandler(workerContext, target._index_))); + } else { + if (isChangeEvent) { + changeEventSubscribed = false; + } + (target as HTMLElement).removeEventListener(type, knownListeners[index]); + } + if (shouldTrack && !changeEventSubscribed) { + applyDefaultChangeListener(workerContext, target as RenderableElement); + } + }; + + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + const addEventListenerCount = mutations[startPosition + EventSubscriptionMutationIndex.AddEventListenerCount]; + const removeEventListenerCount = mutations[startPosition + EventSubscriptionMutationIndex.RemoveEventListenerCount]; + const addEventListenersPosition = startPosition + EventSubscriptionMutationIndex.Events + removeEventListenerCount * EVENT_SUBSCRIPTION_LENGTH; + const endPosition = + startPosition + EventSubscriptionMutationIndex.Events + (addEventListenerCount + removeEventListenerCount) * EVENT_SUBSCRIPTION_LENGTH; + + if (target) { + for (let iterator = startPosition + EventSubscriptionMutationIndex.Events; iterator < endPosition; iterator += EVENT_SUBSCRIPTION_LENGTH) { + processListenerChange(target, iterator <= addEventListenersPosition, strings.get(mutations[iterator]), mutations[iterator + 1]); + } + } else { + console.error(`getNode() yields null – ${target}`); + } + + return endPosition; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + const addEventListenerCount = mutations[startPosition + EventSubscriptionMutationIndex.AddEventListenerCount]; + const removeEventListenerCount = mutations[startPosition + EventSubscriptionMutationIndex.RemoveEventListenerCount]; + const addEventListenersPosition = startPosition + EventSubscriptionMutationIndex.Events + removeEventListenerCount * EVENT_SUBSCRIPTION_LENGTH; + const endPosition = + startPosition + EventSubscriptionMutationIndex.Events + (addEventListenerCount + removeEventListenerCount) * EVENT_SUBSCRIPTION_LENGTH; + + let removedEventListeners: Array<{ type: string; index: number }> = []; + let addedEventListeners: Array<{ type: string; index: number }> = []; + + for (let iterator = startPosition + EventSubscriptionMutationIndex.Events; iterator < endPosition; iterator += EVENT_SUBSCRIPTION_LENGTH) { + const eventList = iterator <= addEventListenersPosition ? addedEventListeners : removedEventListeners; + eventList.push({ + type: strings.get(mutations[iterator]), + index: mutations[iterator + 1], + }); + } + + return { + target, + removedEventListeners, + addedEventListeners, + }; + }, + }; +} diff --git a/src/main-thread/worker-dom.ts b/src/main-thread/commands/interface.ts similarity index 73% rename from src/main-thread/worker-dom.ts rename to src/main-thread/commands/interface.ts index 14a873455..9ffd6c5cc 100644 --- a/src/main-thread/worker-dom.ts +++ b/src/main-thread/commands/interface.ts @@ -14,19 +14,7 @@ * limitations under the License. */ -export class WorkerDom { - private worker: Worker; - - /** - */ - constructor(worker: Worker) { - this.worker = worker; - } - - /** - * Terminates the worker-dom completely. - */ - terminate() { - this.worker.terminate(); - } +export interface CommandExecutor { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number; + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object; } diff --git a/src/main-thread/commands/long-task.ts b/src/main-thread/commands/long-task.ts index fc0f6ed63..14c530d35 100644 --- a/src/main-thread/commands/long-task.ts +++ b/src/main-thread/commands/long-task.ts @@ -14,55 +14,44 @@ * limitations under the License. */ -import { LongTaskFunction, WorkerCallbacks } from '../callbacks'; -import { TransferrableMutationRecord } from '../../transfer/TransferrableRecord'; +import { WorkerDOMConfiguration } from '../configuration'; +import { CommandExecutor } from './interface'; +import { TransferrableMutationType, ReadableMutationType, LongTaskMutationIndex } from '../../transfer/TransferrableMutation'; -export class LongTaskProcessor { - private onLongTask: LongTaskFunction | null; - private currentResolver: Function | null; - private index: number; - - constructor(callbacks?: WorkerCallbacks) { - this.onLongTask = (callbacks && callbacks.onLongTask) || null; - this.currentResolver = null; - this.index = 0; - } - - isInLongTask(): boolean { - return !!this.currentResolver; - } +export interface LongTaskCommandExecutor extends CommandExecutor { + active: boolean; +} - /** - * Process commands transfered from worker thread to main thread. - * @param mutation mutation record containing commands to execute. - */ - processStart = (mutation?: TransferrableMutationRecord): void => { - if (!this.onLongTask) { - return; - } - this.index++; - if (!this.currentResolver) { - this.onLongTask( - new Promise(resolve => { - this.currentResolver = resolve; - }), - ); - } - }; +export function LongTaskExecutor(config: WorkerDOMConfiguration): LongTaskCommandExecutor { + let index: number = 0; + let currentResolver: Function | null; - /** - * Process commands transfered from worker thread to main thread. - * @param mutation mutation record containing commands to execute. - */ - processEnd = (mutation?: TransferrableMutationRecord): void => { - if (!this.onLongTask) { - return; - } - this.index--; - if (this.currentResolver && this.index <= 0) { - this.currentResolver(); - this.currentResolver = null; - this.index = 0; - } + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + if (config.longTask) { + if (mutations[startPosition] === TransferrableMutationType.LONG_TASK_START) { + index++; + if (!currentResolver) { + config.longTask(new Promise(resolve => (currentResolver = resolve))); + } + } else if (mutations[startPosition] === TransferrableMutationType.LONG_TASK_END) { + index--; + if (currentResolver && index <= 0) { + currentResolver(); + currentResolver = null; + index = 0; + } + } + } + return startPosition + LongTaskMutationIndex.End; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + return { + type: ReadableMutationType[mutations[startPosition]], + }; + }, + get active(): boolean { + return currentResolver !== null; + }, }; } diff --git a/src/main-thread/commands/property.ts b/src/main-thread/commands/property.ts new file mode 100644 index 000000000..9405a8593 --- /dev/null +++ b/src/main-thread/commands/property.ts @@ -0,0 +1,52 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PropertyMutationIndex } from '../../transfer/TransferrableMutation'; +import { CommandExecutor } from './interface'; +import { Strings } from '../strings'; +import { WorkerDOMConfiguration } from '../configuration'; + +export function PropertyProcessor(strings: Strings, config: WorkerDOMConfiguration): CommandExecutor { + return { + execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number { + const name = strings.get(mutations[startPosition + PropertyMutationIndex.Name]); + const value = + (mutations[startPosition + PropertyMutationIndex.Value] !== 0 && strings.get(mutations[startPosition + PropertyMutationIndex.Value])) || null; + if (name && value != null) { + const stringValue = String(value); + if (!config.sanitizer || config.sanitizer.validProperty(target.nodeName, name, stringValue)) { + // TODO(choumx, #122): Proper support for non-string property mutations. + const isBooleanProperty = name == 'checked'; + target[name] = isBooleanProperty ? value === 'true' : value; + } else { + // TODO(choumx): Inform worker that sanitizer ignored unsafe property value change. + } + } + return startPosition + PropertyMutationIndex.End; + }, + print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object { + const name = strings.get(mutations[startPosition + PropertyMutationIndex.Name]); + const value = + (mutations[startPosition + PropertyMutationIndex.Value] !== 0 && strings.get(mutations[startPosition + PropertyMutationIndex.Value])) || null; + + return { + target, + name, + value, + }; + }, + }; +} diff --git a/src/main-thread/callbacks.ts b/src/main-thread/configuration.ts similarity index 79% rename from src/main-thread/callbacks.ts rename to src/main-thread/configuration.ts index c7804c646..26a4ccef0 100644 --- a/src/main-thread/callbacks.ts +++ b/src/main-thread/configuration.ts @@ -15,7 +15,7 @@ */ import { MessageFromWorker, MessageToWorker } from '../transfer/Messages'; -import { Phase } from '../transfer/phase'; +import { Phase } from '../transfer/Phase'; /** * The callback for `onMutationPump`. If specified, this callback will be called @@ -24,11 +24,22 @@ import { Phase } from '../transfer/phase'; */ export type MutationPumpFunction = (flush: Function, phase: Phase) => void; -/** - */ export type LongTaskFunction = (promise: Promise) => void; -export interface WorkerCallbacks { +export interface WorkerDOMConfiguration { + // ---- Required Values. + authorURL: string; + domURL: string; + + // ---- Optional Overrides + // Schedules mutation phase. + mutationPump?: MutationPumpFunction; + // Schedules long task. + longTask?: LongTaskFunction; + // Sanitizer for DOM Mutations + sanitizer?: Sanitizer; + + // ---- Optional Callbacks // Called when worker consumes the page's initial DOM state. onCreateWorker?: (initialDOM: RenderableElement) => void; // Called when the worker is hydrated (sends a HYDRATE message). @@ -37,8 +48,4 @@ export interface WorkerCallbacks { onSendMessage?: (message: MessageToWorker) => void; // Called after a message is received from the worker. onReceiveMessage?: (message: MessageFromWorker) => void; - // Called to schedule mutation phase. See `MutationPumpFunction`. - onMutationPump?: MutationPumpFunction; - // Called to schedule long task. See `LongTaskFunction`. - onLongTask?: LongTaskFunction; } diff --git a/src/main-thread/debugging.ts b/src/main-thread/debugging.ts index 24ab2dfb9..328f0cb1e 100644 --- a/src/main-thread/debugging.ts +++ b/src/main-thread/debugging.ts @@ -22,165 +22,25 @@ * @see https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#9.4 */ -import { - EventToWorker, - MessageFromWorker, - MessageType, - MessageToWorker, - ValueSyncToWorker, - BoundingClientRectToWorker, - LongTaskStartToWorker, - LongTaskEndToWorker, -} from '../transfer/Messages'; -import { HydrateableNode, TransferredNode } from '../transfer/TransferrableNodes'; +import { EventToWorker, MessageType, MessageToWorker, ValueSyncToWorker, BoundingClientRectToWorker } from '../transfer/Messages'; +import { HydrateableNode, TransferredNode, TransferrableNodeIndex } from '../transfer/TransferrableNodes'; import { NodeContext } from './nodes'; import { TransferrableEvent } from '../transfer/TransferrableEvent'; -import { Strings } from './strings'; -import { TransferrableMutationRecord } from '../transfer/TransferrableRecord'; import { TransferrableKeys } from '../transfer/TransferrableKeys'; import { TransferrableSyncValue } from '../transfer/TransferrableSyncValue'; import { createReadableHydrateableRootNode } from './serialize'; /** - * Reverse mapping of src/worker-thread/MutationRecord.MutationRecord enum. + * @param element */ -const MUTATION_RECORD_TYPE_REVERSE_MAPPING = { - '0': 'ATTRIBUTES', - '1': 'CHARACTER_DATA', - '2': 'CHILD_LIST', - '3': 'PROPERTIES', - '4': 'EVENT_SUBSCRIPTION', - '5': 'GET_BOUNDING_CLIENT_RECT', - '6': 'LONG_TASK_START', - '7': 'LONG_TASK_END', -}; - -export class DebuggingContext { - private strings: Strings; - private nodeContext: NodeContext; - - constructor(strings: Strings, nodeContext: NodeContext) { - this.strings = strings; - this.nodeContext = nodeContext; - } - - /** - * @param element - */ - readableHydrateableNodeFromElement(element: RenderableElement): Object { - const node = createReadableHydrateableRootNode(element); - return readableHydrateableNode(node); - } - - /** - * @param message - */ - readableMessageFromWorker(message: MessageFromWorker): Object { - const { data } = message; - - if (data[TransferrableKeys.type] === MessageType.MUTATE || data[TransferrableKeys.type] === MessageType.HYDRATE) { - const mutations = data[TransferrableKeys.mutations]; - const mutate: any = { - type: data[TransferrableKeys.type] === MessageType.MUTATE ? 'MUTATE' : 'HYDRATE', - mutations: mutations.map(n => this.readableTransferrableMutationRecord(n)), - // Omit 'strings' key. - }; - // TODO(choumx): Like 'strings', I'm not sure 'nodes' is actually useful. - // const nodes = data[TransferrableKeys.nodes]; - // mutate['nodes'] = nodes.map(n => readableTransferrableNode(n)); - return mutate; - } else { - return 'Unrecognized MessageFromWorker type: ' + data[TransferrableKeys.type]; - } - } - - /** - * @param message - */ - readableMessageToWorker(message: MessageToWorker): Object { - if (isEvent(message)) { - const event = message[TransferrableKeys.event]; - return { - type: 'EVENT', - event: readableTransferrableEvent(this.nodeContext, event), - }; - } else if (isValueSync(message)) { - const sync = message[TransferrableKeys.sync]; - return { - type: 'SYNC', - sync: readableTransferrableSyncValue(this.nodeContext, sync), - }; - } else if (isBoundingClientRect(message)) { - return { - type: 'GET_BOUNDING_CLIENT_RECT', - target: readableTransferredNode(this.nodeContext, message[TransferrableKeys.target]), - }; - } else if (isLongTaskStart(message)) { - return { type: 'LONG_TASK_START' }; - } else if (isLongTaskEnd(message)) { - return { type: 'LONG_TASK_END' }; - } else { - return 'Unrecognized MessageToWorker type: ' + message[TransferrableKeys.type]; - } - } - - /** - * @param r - */ - private readableTransferrableMutationRecord(r: TransferrableMutationRecord): Object { - const target = r[TransferrableKeys.target]; - - const out: any = { - type: MUTATION_RECORD_TYPE_REVERSE_MAPPING[r[TransferrableKeys.type]], - target: this.nodeContext.getNode(target) || target, - }; - const added = r[TransferrableKeys.addedNodes]; - if (added) { - out['addedNodes'] = added.map(n => readableTransferredNode(this.nodeContext, n)); - } - const removed = r[TransferrableKeys.removedNodes]; - if (removed) { - out['removedNodes'] = removed.map(n => readableTransferredNode(this.nodeContext, n)); - } - const previousSibling = r[TransferrableKeys.previousSibling]; - if (previousSibling) { - out['previousSibling'] = previousSibling; - } - const nextSibling = r[TransferrableKeys.nextSibling]; - if (nextSibling) { - out['nextSibling'] = nextSibling; - } - const attributeName = r[TransferrableKeys.attributeName]; - if (attributeName !== undefined) { - out['attributeName'] = this.strings.get(attributeName); - } - const attributeNamespace = r[TransferrableKeys.attributeNamespace]; - if (attributeNamespace !== undefined) { - out['attributeNamespace'] = attributeNamespace; - } - const propertyName = r[TransferrableKeys.propertyName]; - if (propertyName !== undefined) { - out['propertyName'] = propertyName; - } - const value = r[TransferrableKeys.value]; - if (value !== undefined) { - out['value'] = this.strings.get(value); - } - const oldValue = r[TransferrableKeys.oldValue]; - if (oldValue !== undefined) { - out['oldValue'] = this.strings.get(oldValue); - } - const addedEvents = r[TransferrableKeys.addedEvents]; - if (addedEvents !== undefined) { - out['addedEvents'] = addedEvents; - } - const removedEvents = r[TransferrableKeys.removedEvents]; - if (removedEvents !== undefined) { - out['removedEvents'] = removedEvents; - } - return out; - } -} +export const readableHydrateableRootNode = (element: RenderableElement): Object => + readableHydrateableNode(createReadableHydrateableRootNode(element)); +/** + * @param nodeContext {NodeContext} + * @param node {TransferredNode} + */ +export const readableTransferredNode = (nodeContext: NodeContext, node: TransferredNode): Object | number | null => + (node != null && nodeContext.getNode(node[TransferrableNodeIndex.Index])) || node; /** * @param node @@ -189,127 +49,96 @@ function readableHydrateableNode(node: HydrateableNode): Object { const out: any = { nodeType: node[TransferrableKeys.nodeType], name: node[TransferrableKeys.localOrNodeName], + attributes: null, + childNodes: null, }; + const attributes = node[TransferrableKeys.attributes]; if (attributes) { - out['attributes'] = attributes.map(attr => { - return { - name: attr[1], - value: attr[2], - }; - }); + out.attributes = attributes.map(attr => ({ + name: attr[1], + value: attr[2], + })); } + const childNodes = node[TransferrableKeys.childNodes]; if (childNodes) { - out['childNodes'] = childNodes.map(child => readableHydrateableNode(child)); + out.childNodes = childNodes.map(readableHydrateableNode); } - return out; -} - -/** - * @param n - */ -function readableTransferredNode(nodeContext: NodeContext, n: TransferredNode): Object { - const index = n[TransferrableKeys.index]; - return nodeContext.getNode(index) || index; -} -/** - * @param data - */ -function isEvent(message: MessageToWorker): message is EventToWorker { - return message[TransferrableKeys.type] == MessageType.EVENT; -} - -/** - * @param data - */ -function isValueSync(message: MessageToWorker): message is ValueSyncToWorker { - return message[TransferrableKeys.type] == MessageType.SYNC; + return out; } /** - * @param data + * @param message {MessageToWorker} */ -function isBoundingClientRect(message: MessageToWorker): message is BoundingClientRectToWorker { - return message[TransferrableKeys.type] === MessageType.GET_BOUNDING_CLIENT_RECT; -} +const isEvent = (message: MessageToWorker): message is EventToWorker => message[TransferrableKeys.type] == MessageType.EVENT; +const isValueSync = (message: MessageToWorker): message is ValueSyncToWorker => message[TransferrableKeys.type] == MessageType.SYNC; +const isBoundingClientRect = (message: MessageToWorker): message is BoundingClientRectToWorker => + message[TransferrableKeys.type] === MessageType.GET_BOUNDING_CLIENT_RECT; /** - * @param data + * @param nodeContext {NodeContext} + * @param event {TransferrableEvent} */ -function isLongTaskStart(message: MessageToWorker): message is LongTaskStartToWorker { - return message[TransferrableKeys.type] === MessageType.LONG_TASK_START; -} +function readableTransferrableEvent(nodeContext: NodeContext, event: TransferrableEvent): Object { + const value = (item?: null | number | boolean | TransferredNode): number | boolean | Object | null => { + if (typeof item === 'number' || typeof item === 'boolean') { + return item !== undefined ? item : null; + } + return item !== undefined && item !== null ? readableTransferredNode(nodeContext, item) : null; + }; -/** - * @param data - */ -function isLongTaskEnd(message: MessageToWorker): message is LongTaskEndToWorker { - return message[TransferrableKeys.type] === MessageType.LONG_TASK_END; + return { + type: event[TransferrableKeys.type], + bubbles: value(event[TransferrableKeys.bubbles]), + cancelable: value(event[TransferrableKeys.cancelable]), + cancelBubble: value(event[TransferrableKeys.cancelBubble]), + defaultPrevented: value(event[TransferrableKeys.defaultPrevented]), + eventPhase: value(event[TransferrableKeys.eventPhase]), + isTrusted: value(event[TransferrableKeys.isTrusted]), + returnValue: value(event[TransferrableKeys.returnValue]), + currentTarget: value(event[TransferrableKeys.currentTarget]), + target: value(event[TransferrableKeys.target]), + scoped: value(event[TransferrableKeys.scoped]), + keyCode: value(event[TransferrableKeys.keyCode]), + }; } /** - * @param e + * @param nodeContext {NodeContext} + * @param value {TransferrableSyncValue} */ -function readableTransferrableEvent(nodeContext: NodeContext, e: TransferrableEvent): Object { - const out: any = { - type: e[TransferrableKeys.type], +function readableTransferrableSyncValue(nodeContext: NodeContext, value: TransferrableSyncValue): Object { + const index = value[TransferrableKeys.index]; + return { + target: nodeContext.getNode(index) || index, + value: value[TransferrableKeys.value], }; - const bubbles = e[TransferrableKeys.bubbles]; - if (bubbles !== undefined) { - out['bubbles'] = bubbles; - } - const cancelable = e[TransferrableKeys.cancelable]; - if (cancelable !== undefined) { - out['cancelable'] = cancelable; - } - const cancelBubble = e[TransferrableKeys.cancelBubble]; - if (cancelBubble !== undefined) { - out['cancelBubble'] = cancelBubble; - } - const defaultPrevented = e[TransferrableKeys.defaultPrevented]; - if (defaultPrevented !== undefined) { - out['defaultPrevented'] = defaultPrevented; - } - const eventPhase = e[TransferrableKeys.eventPhase]; - if (eventPhase !== undefined) { - out['eventPhase'] = eventPhase; - } - const isTrusted = e[TransferrableKeys.isTrusted]; - if (isTrusted !== undefined) { - out['isTrusted'] = isTrusted; - } - const returnValue = e[TransferrableKeys.returnValue]; - if (returnValue !== undefined) { - out['returnValue'] = returnValue; - } - const currentTarget = e[TransferrableKeys.currentTarget]; - if (currentTarget) { - out['currentTarget'] = readableTransferredNode(nodeContext, currentTarget); - } - const target = e[TransferrableKeys.target]; - if (target) { - out['target'] = readableTransferredNode(nodeContext, target); - } - const scoped = e[TransferrableKeys.scoped]; - if (scoped !== undefined) { - out['scoped'] = scoped; - } - const keyCode = e[TransferrableKeys.keyCode]; - if (keyCode !== undefined) { - out['keyCode'] = keyCode; - } - return out; } /** - * @param v + * @param message */ -function readableTransferrableSyncValue(nodeContext: NodeContext, v: TransferrableSyncValue): Object { - const index = v[TransferrableKeys.index]; - return { - target: nodeContext.getNode(index) || index, - value: v[TransferrableKeys.value], - }; +export function readableMessageToWorker(nodeContext: NodeContext, message: MessageToWorker): Object { + if (isEvent(message)) { + const event = message[TransferrableKeys.event]; + return { + type: 'EVENT', + event: readableTransferrableEvent(nodeContext, event), + }; + } else if (isValueSync(message)) { + const sync = message[TransferrableKeys.sync]; + return { + type: 'SYNC', + sync: readableTransferrableSyncValue(nodeContext, sync), + }; + } else if (isBoundingClientRect(message)) { + return { + type: 'GET_BOUNDING_CLIENT_RECT', + target: readableTransferredNode(nodeContext, message[TransferrableKeys.target]), + }; + } else { + return 'Unrecognized MessageToWorker type: ' + message[TransferrableKeys.type]; + } } diff --git a/src/main-thread/index.safe.ts b/src/main-thread/index.safe.ts index cee9e41d6..715c87e95 100644 --- a/src/main-thread/index.safe.ts +++ b/src/main-thread/index.safe.ts @@ -15,21 +15,25 @@ */ import { DOMPurifySanitizer } from './DOMPurifySanitizer'; -import { WorkerCallbacks } from './callbacks'; -import { WorkerDom } from './worker-dom'; import { fetchAndInstall, install } from './install'; +import { WorkerDOMConfiguration, LongTaskFunction } from './configuration'; /** Users can import this and configure the sanitizer with custom DOMPurify hooks, etc. */ export const sanitizer = new DOMPurifySanitizer(); /** * @param baseElement - * @param workerDOMUrl + * @param domURL */ -export function upgradeElement(baseElement: Element, workerDOMUrl: string, callbacks?: WorkerCallbacks, debug?: boolean): Promise { +export function upgradeElement(baseElement: Element, domURL: string, longTask?: LongTaskFunction): Promise { const authorURL = baseElement.getAttribute('src'); if (authorURL) { - return fetchAndInstall(baseElement as HTMLElement, authorURL, workerDOMUrl, callbacks, sanitizer, debug); + return fetchAndInstall(baseElement as HTMLElement, { + domURL, + authorURL, + sanitizer, + longTask, + }); } return Promise.resolve(null); } @@ -37,15 +41,9 @@ export function upgradeElement(baseElement: Element, workerDOMUrl: string, callb /** * This function's API will likely change frequently. Use at your own risk! * @param baseElement - * @param fetchPromise Promise that resolves with a tuple containing the worker script, author script, and author script URL. + * @param fetchPromise Promise that resolves containing worker script, and author script. */ -export function upgrade( - baseElement: Element, - fetchPromise: Promise<[string, string, string]>, - callbacks?: WorkerCallbacks, - debug?: boolean, -): Promise { - return install(fetchPromise, baseElement as HTMLElement, callbacks, sanitizer, debug); +export function upgrade(baseElement: Element, fetchPromise: Promise<[string, string]>, config: WorkerDOMConfiguration): Promise { + config.sanitizer = sanitizer; + return install(fetchPromise, baseElement as HTMLElement, config); } - -export { WorkerDom }; diff --git a/src/main-thread/index.ts b/src/main-thread/index.ts index 2552de2fa..a7d8f5db4 100644 --- a/src/main-thread/index.ts +++ b/src/main-thread/index.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import { WorkerCallbacks } from './callbacks'; -import { WorkerDom } from './worker-dom'; import { fetchAndInstall } from './install'; -export function upgradeElement(baseElement: Element, workerDOMUrl: string, callbacks?: WorkerCallbacks): Promise { +export function upgradeElement(baseElement: Element, domURL: string): Promise { const authorURL = baseElement.getAttribute('src'); if (authorURL) { - return fetchAndInstall(baseElement as HTMLElement, authorURL, workerDOMUrl, callbacks); + fetchAndInstall(baseElement as HTMLElement, { + authorURL, + domURL, + }); } return Promise.resolve(null); } - -export { WorkerDom }; diff --git a/src/main-thread/install.ts b/src/main-thread/install.ts index d18ca4eb7..d56c20121 100644 --- a/src/main-thread/install.ts +++ b/src/main-thread/install.ts @@ -14,16 +14,13 @@ * limitations under the License. */ -import { DebuggingContext } from './debugging'; import { MutationFromWorker, MessageType, MessageFromWorker } from '../transfer/Messages'; import { MutatorProcessor } from './mutator'; import { NodeContext } from './nodes'; -import { Phase } from '../transfer/phase'; import { Strings } from './strings'; import { TransferrableKeys } from '../transfer/TransferrableKeys'; -import { WorkerCallbacks } from './callbacks'; +import { WorkerDOMConfiguration } from './configuration'; import { WorkerContext } from './worker'; -import { WorkerDom } from './worker-dom'; const ALLOWABLE_MESSAGE_TYPES = [MessageType.MUTATE, MessageType.HYDRATE]; @@ -35,106 +32,51 @@ const ALLOWABLE_MESSAGE_TYPES = [MessageType.MUTATE, MessageType.HYDRATE]; * @param sanitizer * @param debug */ -export function fetchAndInstall( - baseElement: HTMLElement, - authorScriptURL: string, - workerDOMURL: string, - callbacks?: WorkerCallbacks, - sanitizer?: Sanitizer, - debug?: boolean, -): Promise { +export function fetchAndInstall(baseElement: HTMLElement, config: WorkerDOMConfiguration): Promise { const fetchPromise = Promise.all([ // TODO(KB): Fetch Polyfill for IE11. - fetch(workerDOMURL).then(response => response.text()), - fetch(authorScriptURL).then(response => response.text()), - Promise.resolve(authorScriptURL), + fetch(config.domURL).then(response => response.text()), + fetch(config.authorURL).then(response => response.text()), ]); - return install(fetchPromise, baseElement, callbacks, sanitizer, debug); + return install(fetchPromise, baseElement, config); } /** * @param fetchPromise * @param baseElement - * @param callbacks - * @param sanitizer - * @param debug + * @param config */ -export function install( - fetchPromise: Promise<[string, string, string]>, - baseElement: HTMLElement, - callbacks?: WorkerCallbacks, - sanitizer?: Sanitizer, - debug?: boolean, -): Promise { +export function install(fetchPromise: Promise<[string, string]>, baseElement: HTMLElement, config: WorkerDOMConfiguration): Promise { const strings = new Strings(); const nodeContext = new NodeContext(strings, baseElement); - if (debug) { - const debuggingContext = new DebuggingContext(strings, nodeContext); - callbacks = wrapCallbacks(debuggingContext, callbacks); - } - return fetchPromise.then(([workerDOMScript, authorScript, authorScriptURL]) => { - if (workerDOMScript && authorScript && authorScriptURL) { - const workerContext = new WorkerContext(baseElement, workerDOMScript, authorScript, authorScriptURL, callbacks); - const worker = workerContext.getWorker(); - const mutatorContext = new MutatorProcessor(strings, nodeContext, workerContext, callbacks, sanitizer); - worker.onmessage = (message: MessageFromWorker) => { + return fetchPromise.then(([domScriptContent, authorScriptContent]) => { + if (domScriptContent && authorScriptContent && config.authorURL) { + const workerContext = new WorkerContext(baseElement, nodeContext, domScriptContent, authorScriptContent, config); + const mutatorContext = new MutatorProcessor(strings, nodeContext, workerContext, config); + workerContext.worker.onmessage = (message: MessageFromWorker) => { const { data } = message; - const type = data[TransferrableKeys.type]; - if (!ALLOWABLE_MESSAGE_TYPES.includes(type)) { + + if (!ALLOWABLE_MESSAGE_TYPES.includes(data[TransferrableKeys.type])) { return; } - const phase = type == MessageType.HYDRATE ? Phase.Hydrating : Phase.Mutating; mutatorContext.mutate( - phase, + (data as MutationFromWorker)[TransferrableKeys.phase], (data as MutationFromWorker)[TransferrableKeys.nodes], (data as MutationFromWorker)[TransferrableKeys.strings], - (data as MutationFromWorker)[TransferrableKeys.mutations], + new Uint16Array(data[TransferrableKeys.mutations]), ); + // Invoke callbacks after hydrate/mutate processing so strings etc. are stored. - if (callbacks) { - if (phase === Phase.Hydrating && callbacks.onHydration) { - callbacks.onHydration(); - } - if (callbacks.onReceiveMessage) { - callbacks.onReceiveMessage(message); - } + if (data[TransferrableKeys.type] === MessageType.HYDRATE && config.onHydration) { + config.onHydration(); + } + if (config.onReceiveMessage) { + config.onReceiveMessage(message); } }; - return new WorkerDom(worker); + + return workerContext.worker; } return null; }); } - -// TODO(dvoytenko): reverse the dependency direction so that we can remove -// debugging.ts from other entry points. -function wrapCallbacks(debuggingContext: DebuggingContext, callbacks?: WorkerCallbacks): WorkerCallbacks { - return { - onCreateWorker(initialDOM) { - if (callbacks && callbacks.onCreateWorker) { - const readable = debuggingContext.readableHydrateableNodeFromElement(initialDOM); - callbacks.onCreateWorker(readable as any); - } - }, - onHydration() { - if (callbacks && callbacks.onHydration) { - callbacks.onHydration(); - } - }, - onSendMessage(message) { - if (callbacks && callbacks.onSendMessage) { - const readable = debuggingContext.readableMessageToWorker(message); - callbacks.onSendMessage(readable as any); - } - }, - onReceiveMessage(message) { - if (callbacks && callbacks.onReceiveMessage) { - const readable = debuggingContext.readableMessageFromWorker(message); - callbacks.onReceiveMessage(readable as any); - } - }, - // Passthrough callbacks: - onMutationPump: callbacks && callbacks.onMutationPump, - onLongTask: callbacks && callbacks.onLongTask, - }; -} diff --git a/src/main-thread/main-thread.d.ts b/src/main-thread/main-thread.d.ts index be8ec9a4a..bf4b2de73 100644 --- a/src/main-thread/main-thread.d.ts +++ b/src/main-thread/main-thread.d.ts @@ -37,3 +37,5 @@ interface Node { } type RenderableElement = (HTMLElement | SVGElement | Text | Comment) & { [index: string]: any }; + +declare const DEBUG_ENABLED: boolean; diff --git a/src/main-thread/mutator.ts b/src/main-thread/mutator.ts index 0c805ba09..ddb542074 100644 --- a/src/main-thread/mutator.ts +++ b/src/main-thread/mutator.ts @@ -14,31 +14,30 @@ * limitations under the License. */ -import { BoundingClientRectProcessor } from './commands/bounding-client-rect'; -import { EventSubscriptionProcessor } from './commands/event-subscription'; -import { LongTaskProcessor } from './commands/long-task'; -import { MutationPumpFunction, WorkerCallbacks } from './callbacks'; -import { MutationRecordType } from '../worker-thread/MutationRecord'; import { NodeContext } from './nodes'; -import { Phase } from '../transfer/phase'; import { Strings } from './strings'; -import { TransferrableMutationRecord } from '../transfer/TransferrableRecord'; -import { TransferrableKeys } from '../transfer/TransferrableKeys'; -import { TransferrableNode } from '../transfer/TransferrableNodes'; import { WorkerContext } from './worker'; +import { TransferrableMutationType } from '../transfer/TransferrableMutation'; +import { EventSubscriptionProcessor } from './commands/event-subscription'; +import { BoundingClientRectProcessor } from './commands/bounding-client-rect'; +import { ChildListProcessor } from './commands/child-list'; +import { AttributeProcessor } from './commands/attribute'; +import { CharacterDataProcessor } from './commands/character-data'; +import { PropertyProcessor } from './commands/property'; +import { LongTaskExecutor } from './commands/long-task'; +import { CommandExecutor } from './commands/interface'; +import { WorkerDOMConfiguration, MutationPumpFunction } from './configuration'; +import { Phase } from '../transfer/Phase'; export class MutatorProcessor { private strings: Strings; private nodeContext: NodeContext; - private mutationPump: MutationPumpFunction; - private boundSyncFlush: () => void; - private mutationQueue: Array; - private stringQueue: Array; - private nodeQueue: Array; - private pendingQueue: boolean; + private mutationQueue: Array = []; + private pendingMutations: boolean = false; + private mutationPumpFunction: MutationPumpFunction; private sanitizer: Sanitizer | undefined; - private mutators: { - [key: number]: (mutation: TransferrableMutationRecord, target: Node) => void; + private executors: { + [key: number]: CommandExecutor; }; /** @@ -47,47 +46,39 @@ export class MutatorProcessor { * @param workerContext * @param sanitizer Sanitizer to apply to content if needed. */ - constructor(strings: Strings, nodeContext: NodeContext, workerContext: WorkerContext, callbacks?: WorkerCallbacks, sanitizer?: Sanitizer) { + constructor(strings: Strings, nodeContext: NodeContext, workerContext: WorkerContext, config: WorkerDOMConfiguration) { this.strings = strings; this.nodeContext = nodeContext; - this.sanitizer = sanitizer; - this.mutationPump = (callbacks && callbacks.onMutationPump) || requestAnimationFrame.bind(null); - this.boundSyncFlush = this.syncFlush.bind(this); - this.stringQueue = []; - this.nodeQueue = []; - this.mutationQueue = []; - this.pendingQueue = false; - - const eventSubscriptionProcessor = new EventSubscriptionProcessor(strings, nodeContext, workerContext); - const boundingClientRectProcessor = new BoundingClientRectProcessor(nodeContext, workerContext); - const longTaskProcessor = new LongTaskProcessor(callbacks); - - this.mutators = { - [MutationRecordType.CHILD_LIST]: this.mutateChildList.bind(this), - [MutationRecordType.ATTRIBUTES]: this.mutateAttributes.bind(this), - [MutationRecordType.CHARACTER_DATA]: this.mutateCharacterData.bind(this), - [MutationRecordType.PROPERTIES]: this.mutateProperties.bind(this), - [MutationRecordType.EVENT_SUBSCRIPTION]: eventSubscriptionProcessor.process.bind(eventSubscriptionProcessor), - [MutationRecordType.GET_BOUNDING_CLIENT_RECT]: boundingClientRectProcessor.process.bind(boundingClientRectProcessor), - [MutationRecordType.LONG_TASK_START]: longTaskProcessor.processStart, - [MutationRecordType.LONG_TASK_END]: longTaskProcessor.processEnd, + this.sanitizer = config.sanitizer; + this.mutationPumpFunction = config.mutationPump || requestAnimationFrame.bind(null); + + const LongTaskExecutorInstance = LongTaskExecutor(config); + this.executors = { + [TransferrableMutationType.CHILD_LIST]: ChildListProcessor(nodeContext), + [TransferrableMutationType.ATTRIBUTES]: AttributeProcessor(strings, config), + [TransferrableMutationType.CHARACTER_DATA]: CharacterDataProcessor(strings), + [TransferrableMutationType.PROPERTIES]: PropertyProcessor(strings, config), + [TransferrableMutationType.EVENT_SUBSCRIPTION]: EventSubscriptionProcessor(strings, workerContext), + [TransferrableMutationType.GET_BOUNDING_CLIENT_RECT]: BoundingClientRectProcessor(workerContext), + [TransferrableMutationType.LONG_TASK_START]: LongTaskExecutorInstance, + [TransferrableMutationType.LONG_TASK_END]: LongTaskExecutorInstance, }; } /** * Process MutationRecords from worker thread applying changes to the existing DOM. - * @param phase + * @param phase Current Phase Worker Thread exists in. * @param nodes New nodes to add in the main thread with the incoming mutations. * @param stringValues Additional string values to use in decoding messages. * @param mutations Changes to apply in both graph shape and content of Elements. */ - mutate(phase: Phase, nodes: Array, stringValues: Array, mutations: Array): void { - this.stringQueue = this.stringQueue.concat(stringValues); - this.nodeQueue = this.nodeQueue.concat(nodes); + public mutate(phase: Phase, nodes: ArrayBuffer, stringValues: Array, mutations: Uint16Array): void { + this.strings.storeValues(stringValues); + this.nodeContext.createNodes(nodes, this.sanitizer); this.mutationQueue = this.mutationQueue.concat(mutations); - if (!this.pendingQueue) { - this.pendingQueue = true; - this.mutationPump(this.boundSyncFlush, phase); + if (!this.pendingMutations) { + this.pendingMutations = true; + this.mutationPumpFunction(this.syncFlush, phase); } } @@ -97,100 +88,24 @@ export class MutatorProcessor { * * Investigations in using asyncFlush to resolve are worth considering. */ - private syncFlush(): void { - this.pendingQueue = false; - - this.strings.storeValues(this.stringQueue); - this.stringQueue.length = 0; - - this.nodeQueue.forEach(node => this.nodeContext.createNode(node, this.sanitizer)); - this.nodeQueue.length = 0; - - this.mutationQueue.forEach(mutation => { - const nodeId = mutation[TransferrableKeys.target]; - const node = this.nodeContext.getNode(nodeId); - if (!node) { - console.error('getNode() yields a null value. Node id (' + nodeId + ') was not found.'); - return; - } - this.mutators[mutation[TransferrableKeys.type]](mutation, node); - }); - this.mutationQueue.length = 0; - } - - private mutateChildList(mutation: TransferrableMutationRecord, target: HTMLElement) { - (mutation[TransferrableKeys.removedNodes] || []).forEach(nodeReference => { - const nodeId = nodeReference[TransferrableKeys.index]; - const node = this.nodeContext.getNode(nodeId); - if (!node) { - console.error('getNode() yields a null value. Node id (' + nodeId + ') was not found.'); - return; - } - node.remove(); - }); - - const addedNodes = mutation[TransferrableKeys.addedNodes]; - const nextSibling = mutation[TransferrableKeys.nextSibling]; - if (addedNodes) { - addedNodes.forEach(node => { - let newChild = null; - newChild = this.nodeContext.getNode(node[TransferrableKeys.index]); - - if (!newChild) { - // Transferred nodes that are not stored were previously removed by the sanitizer. - if (node[TransferrableKeys.transferred]) { - return; - } else { - newChild = this.nodeContext.createNode(node as TransferrableNode, this.sanitizer); - } - } - if (newChild) { - target.insertBefore(newChild, (nextSibling && this.nodeContext.getNode(nextSibling[TransferrableKeys.index])) || null); - } else { - // TODO(choumx): Inform worker that sanitizer removed newChild. + private syncFlush = (): void => { + this.mutationQueue.forEach(mutationArray => { + let operationStart: number = 0; + let length: number = mutationArray.length; + + while (operationStart < length) { + const target = this.nodeContext.getNode(mutationArray[operationStart + 1]); + if (!target) { + console.error(`getNode() yields null – ${target}`); + return; } - }); - } - } - - private mutateAttributes(mutation: TransferrableMutationRecord, target: HTMLElement | SVGElement) { - const attributeName = - mutation[TransferrableKeys.attributeName] !== undefined ? this.strings.get(mutation[TransferrableKeys.attributeName] as number) : null; - const value = mutation[TransferrableKeys.value] !== undefined ? this.strings.get(mutation[TransferrableKeys.value] as number) : null; - if (attributeName != null) { - if (value == null) { - target.removeAttribute(attributeName); - } else { - if (!this.sanitizer || this.sanitizer.validAttribute(target.nodeName, attributeName, value)) { - target.setAttribute(attributeName, value); - } else { - // TODO(choumx): Inform worker that sanitizer ignored unsafe attribute value change. + if (DEBUG_ENABLED) { + console.info('debug', 'mutation', this.executors[mutationArray[operationStart]].print(mutationArray, operationStart, target)); } + operationStart = this.executors[mutationArray[operationStart]].execute(mutationArray, operationStart, target); } - } - } - - private mutateCharacterData(mutation: TransferrableMutationRecord, target: CharacterData) { - const value = mutation[TransferrableKeys.value]; - if (value) { - // Sanitization not necessary for textContent. - target.textContent = this.strings.get(value); - } - } - - private mutateProperties(mutation: TransferrableMutationRecord, target: RenderableElement) { - const propertyName = - mutation[TransferrableKeys.propertyName] !== undefined ? this.strings.get(mutation[TransferrableKeys.propertyName] as number) : null; - const value = mutation[TransferrableKeys.value] !== undefined ? this.strings.get(mutation[TransferrableKeys.value] as number) : null; - if (propertyName && value != null) { - const stringValue = String(value); - if (!this.sanitizer || this.sanitizer.validProperty(target.nodeName, propertyName, stringValue)) { - // TODO(choumx, #122): Proper support for non-string property mutations. - const isBooleanProperty = propertyName == 'checked'; - target[propertyName] = isBooleanProperty ? value === 'true' : value; - } else { - // TODO(choumx): Inform worker that sanitizer ignored unsafe property value change. - } - } - } + }); + this.mutationQueue = []; + this.pendingMutations = false; + }; } diff --git a/src/main-thread/nodes.ts b/src/main-thread/nodes.ts index 9e4db4333..dcc12d520 100644 --- a/src/main-thread/nodes.ts +++ b/src/main-thread/nodes.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { TransferrableNode, NodeType } from '../transfer/TransferrableNodes'; -import { TransferrableKeys } from '../transfer/TransferrableKeys'; +import { NodeType, TransferrableNodeIndex } from '../transfer/TransferrableNodes'; import { Strings } from './strings'; export class NodeContext { - private baseElement: HTMLElement; + public baseElement: HTMLElement; private strings: Strings; private count: number; private nodes: Map; @@ -34,8 +33,7 @@ export class NodeContext { this.count = 2; this.strings = strings; - // The nodes map is populated with two default values pointing to - // baseElement. + // The nodes map is populated with two default values pointing to baseElement. // These are [document, document.body] from the worker. this.nodes = new Map([[1, baseElement], [2, baseElement]]); this.baseElement = baseElement as HTMLElement; @@ -47,57 +45,51 @@ export class NodeContext { baseElement.childNodes.forEach(n => this.storeNodes(n)); } - getBaseElement(): HTMLElement { - return this.baseElement; - } + public createNodes = (buffer: ArrayBuffer, sanitizer?: Sanitizer): void => { + const nodeBuffer = new Uint16Array(buffer); + const nodeBufferLength = nodeBuffer.length; - /** - * Create a real DOM Node from a skeleton Object (`{ nodeType, nodeName, attributes, children, data }`) - * @example Text node - * nodeContext.createNode({ nodeType:3, data:'foo' }) - * @example Element node - * nodeContext.createNode({ nodeType:1, nodeName:'div', attributes:[{ name:'a', value:'b' }], childNodes:[ ... ] }) - */ - createNode(skeleton: TransferrableNode, sanitizer?: Sanitizer): Node | null { - let node: Node; - if (skeleton[TransferrableKeys.nodeType] === NodeType.TEXT_NODE) { - node = document.createTextNode(this.strings.get(skeleton[TransferrableKeys.textContent] as number)); - } else if (skeleton[TransferrableKeys.nodeType] === NodeType.DOCUMENT_FRAGMENT_NODE) { - node = document.createDocumentFragment(); - } else if (skeleton[TransferrableKeys.nodeType] === NodeType.COMMENT_NODE) { - node = document.createComment(this.strings.get(skeleton[TransferrableKeys.textContent] as number)); - } else { - const namespace = - skeleton[TransferrableKeys.namespaceURI] !== undefined ? this.strings.get(skeleton[TransferrableKeys.namespaceURI] as number) : undefined; - const localName = this.strings.get(skeleton[TransferrableKeys.localOrNodeName]); - node = namespace ? document.createElementNS(namespace, localName) : document.createElement(localName); + for (let iterator = 0; iterator < nodeBufferLength; iterator += TransferrableNodeIndex.End) { + let node: Node; + if (nodeBuffer[iterator + TransferrableNodeIndex.NodeType] === NodeType.TEXT_NODE) { + node = document.createTextNode(this.strings.get(nodeBuffer[iterator + TransferrableNodeIndex.TextContent])); + } else if (nodeBuffer[iterator + TransferrableNodeIndex.NodeType] === NodeType.COMMENT_NODE) { + node = document.createComment(this.strings.get(nodeBuffer[iterator + TransferrableNodeIndex.TextContent])); + } else if (nodeBuffer[iterator + TransferrableNodeIndex.NodeType] === NodeType.DOCUMENT_FRAGMENT_NODE) { + node = document.createDocumentFragment(); + } else { + const nodeName = this.strings.get(nodeBuffer[iterator + TransferrableNodeIndex.NodeName]); + node = + nodeBuffer[iterator + TransferrableNodeIndex.ContainsNamespace] !== 0 + ? document.createElementNS(this.strings.get(nodeBuffer[iterator + TransferrableNodeIndex.Namespace]), nodeName) + : document.createElement(nodeName); - // TODO(KB): Restore Properties - // skeleton.properties.forEach(property => { - // node[`${property.name}`] = property.value; - // }); - // ((skeleton as TransferrableElement)[TransferrableKeys.childNodes] || []).forEach(childNode => { - // if (childNode[TransferrableKeys.transferred] === NumericBoolean.FALSE) { - // node.appendChild(this.createNode(childNode as TransferrableNode)); - // } - // }); + // TODO(KB): Restore Properties + // skeleton.properties.forEach(property => { + // node[`${property.name}`] = property.value; + // }); + // ((skeleton as TransferrableElement)[TransferrableKeys.childNodes] || []).forEach(childNode => { + // if (childNode[TransferrableKeys.transferred] === NumericBoolean.FALSE) { + // node.appendChild(this.createNode(childNode as TransferrableNode)); + // } + // }); - // If `node` is removed by the sanitizer, don't store it and return null. - if (sanitizer && !sanitizer.sanitize(node)) { - return null; + // If `node` is removed by the sanitizer, don't store it and return null. + if (sanitizer && !sanitizer.sanitize(node)) { + continue; + } } - } - this.storeNode(node, skeleton[TransferrableKeys.index]); - return node; - } + this.storeNode(node, nodeBuffer[iterator]); + } + }; /** * Returns the real DOM Element corresponding to a serialized Element object. * @param id * @return RenderableElement | null */ - getNode(id: number): RenderableElement | null { + public getNode = (id: number): RenderableElement | null => { const node = this.nodes.get(id); if (node && node.nodeName === 'BODY') { @@ -107,16 +99,16 @@ export class NodeContext { return this.baseElement as RenderableElement; } return node as RenderableElement; - } + }; /** * Store the requested node and all of its children. * @param node node to store. */ - private storeNodes(node: Node): void { + private storeNodes = (node: Node): void => { this.storeNode(node, ++this.count); node.childNodes.forEach(n => this.storeNodes(n)); - } + }; /** * Establish link between DOM `node` and worker-generated identifier `id`. diff --git a/src/main-thread/serialize.ts b/src/main-thread/serialize.ts index 1f0672173..a3236f1aa 100644 --- a/src/main-thread/serialize.ts +++ b/src/main-thread/serialize.ts @@ -53,7 +53,7 @@ function createHydrateableNode(element: RenderableElement, minimizeString: (valu export function createHydrateableRootNode(element: RenderableElement): { skeleton: HydrateableNode; strings: Array } { const strings: Array = []; const stringMap: Map = new Map(); - const minimizeString = (value: string): number => { + const storeString = (value: string): number => { if (stringMap.has(value)) { // Safe to cast since we verified the mapping contains the value. return stringMap.get(value) as number; @@ -63,7 +63,7 @@ export function createHydrateableRootNode(element: RenderableElement): { skeleto strings.push(value); return count; }; - const skeleton = createHydrateableNode(element, minimizeString); + const skeleton = createHydrateableNode(element, storeString); return { skeleton, strings }; } diff --git a/src/main-thread/worker.ts b/src/main-thread/worker.ts index f40089914..5bef943cc 100644 --- a/src/main-thread/worker.ts +++ b/src/main-thread/worker.ts @@ -15,26 +15,27 @@ */ import { MessageToWorker } from '../transfer/Messages'; -import { WorkerCallbacks } from './callbacks'; +import { WorkerDOMConfiguration } from './configuration'; import { createHydrateableRootNode } from './serialize'; +import { readableHydrateableRootNode, readableMessageToWorker } from './debugging'; +import { NodeContext } from './nodes'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; export class WorkerContext { - private worker: Worker; - - /** - * Stored callbacks for the most recently created worker. - * Note: This can be easily changed to a lookup table to support multiple workers. - */ - private callbacks: WorkerCallbacks | undefined; + private [TransferrableKeys.worker]: Worker; + private nodeContext: NodeContext; + private config: WorkerDOMConfiguration; /** * @param baseElement + * @param nodeContext * @param workerDOMScript * @param authorScript - * @param authorScriptURL + * @param config */ - constructor(baseElement: HTMLElement, workerDOMScript: string, authorScript: string, authorScriptURL: string, callbacks?: WorkerCallbacks) { - this.callbacks = callbacks; + constructor(baseElement: HTMLElement, nodeContext: NodeContext, workerDOMScript: string, authorScript: string, config: WorkerDOMConfiguration) { + this.nodeContext = nodeContext; + this.config = config; // TODO(KB): Minify this output during build process. const keys: Array = []; @@ -71,23 +72,32 @@ export class WorkerContext { document.observe(); ${authorScript} }).call(WorkerThread.workerDOM); - //# sourceURL=${encodeURI(authorScriptURL)}`; - this.worker = new Worker(URL.createObjectURL(new Blob([code]))); - if (callbacks && callbacks.onCreateWorker) { - callbacks.onCreateWorker(baseElement); + //# sourceURL=${encodeURI(config.authorURL)}`; + this[TransferrableKeys.worker] = new Worker(URL.createObjectURL(new Blob([code]))); + if (DEBUG_ENABLED) { + console.info('debug', 'hydratedNode', readableHydrateableRootNode(baseElement)); + } + if (config.onCreateWorker) { + config.onCreateWorker(baseElement); } } - getWorker(): Worker { - return this.worker; + /** + * Returns the private worker. + */ + get worker(): Worker { + return this[TransferrableKeys.worker]; } /** * @param message */ messageToWorker(message: MessageToWorker) { - if (this.callbacks && this.callbacks.onSendMessage) { - this.callbacks.onSendMessage(message); + if (DEBUG_ENABLED) { + console.info('debug', 'messageToWorker', readableMessageToWorker(this.nodeContext, message)); + } + if (this.config.onSendMessage) { + this.config.onSendMessage(message); } this.worker.postMessage(message); } diff --git a/src/test/Emitter.ts b/src/test/Emitter.ts new file mode 100644 index 000000000..16c046e2e --- /dev/null +++ b/src/test/Emitter.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import test from 'ava'; +import { Document } from '../worker-thread/dom/Document'; +import { MutationFromWorker } from '../transfer/Messages'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; + +type Subscriber = (strings: Array, message: MutationFromWorker, buffers: Array) => void; +export interface Emitter { + once(callback: Subscriber): void; + subscribe(callback: Subscriber): void; + unsubscribe(callback: Subscriber): void; +} + +const strings: Array = []; +export function emitter(document: Document): Emitter { + const subscribers: Map = new Map(); + + function unsubscribe(callback: Subscriber): void { + subscribers.delete(callback); + } + + document.postMessage = (message: MutationFromWorker, buffers: Array) => { + strings.push(...message[TransferrableKeys.strings]); + + let copy = new Map(subscribers); + copy.forEach((once, callback) => { + if (once) { + unsubscribe(callback); + } + callback(strings, message, buffers); + }); + }; + document.observe(); + + return { + once(callback: Subscriber): void { + if (!subscribers.has(callback)) { + subscribers.set(callback, true); + } + }, + subscribe(callback: Subscriber): void { + if (!subscribers.has(callback)) { + subscribers.set(callback, false); + } + }, + unsubscribe, + }; +} + +test('test allowing windows builds to pass', t => { + // Without a test in this file, the transpiled output is picked up by `ava` + // only on Windows systems. + // This means a build will fail because this helper file does not have any found tests. + // EX: 'No tests found in output\test\reflectPropertiesHelper.js' + t.pass(); +}); diff --git a/src/test/element/innerHTML.ts b/src/test/element/innerHTML.ts index 77e041ab1..6a7920f9f 100644 --- a/src/test/element/innerHTML.ts +++ b/src/test/element/innerHTML.ts @@ -134,8 +134,8 @@ test('set element with attributes', t => { node.innerHTML = '
'; const child = node.firstChild!; t.is(child.attributes.length, 1); - t.is(child.attributes[0].name, "hi"); - t.is(child.attributes[0].value, "hello"); + t.is(child.attributes[0].name, 'hi'); + t.is(child.attributes[0].value, 'hello'); }); test('set self closing tags', t => { @@ -149,7 +149,7 @@ test('set self closing tags', t => { test('set invalid html throws', t => { const { node } = t.context; // Use an unclosed tag. - t.throws(() => node.innerHTML = '
'); + t.throws(() => (node.innerHTML = '
')); }); test('set closes tags by closing others', t => { @@ -162,13 +162,12 @@ test('set closes tags by closing others', t => { node.innerHTML = '

'; t.true(node.childNodes.length === 2); t.is(node.innerHTML, '

'); - }); // Some tags will automatically close others. Set innerHTML should consider this behavior, yet // it should not apply for the root element's tags: // https://github.com/ampproject/worker-dom/issues/372 -test('set will alter root element\'s contents, not the element itself', t => { +test("set will alter root element's contents, not the element itself", t => { const document = createDocument(); const pNode = document.createElement('p'); @@ -206,7 +205,7 @@ test('set has svg tag live in SVG namespace', t => { t.is(child.namespaceURI, SVG_NAMESPACE); }); -test('set keeps localName\'s case for tags in SVG namespace', t => { +test("set keeps localName's case for tags in SVG namespace", t => { const { node } = t.context; node.innerHTML = ''; const svgWrapper = node.firstChild!; @@ -226,7 +225,7 @@ test('set handles foreignObject tags correctly', t => { const { node } = t.context; node.innerHTML = '
'; const svgWrapper = node.firstChild!; - + // foreignObject tag lives in SVG namespace const foreignObjectNode = svgWrapper.firstChild!; t.is(foreignObjectNode.namespaceURI, SVG_NAMESPACE); @@ -252,4 +251,4 @@ test('set throws for unsupported namespaces', t => { // the value to set should not be relevant here. node.innerHTML = '
'; }); -}); \ No newline at end of file +}); diff --git a/src/test/htmlelement/serialize.ts b/src/test/htmlelement/serialize.ts deleted file mode 100644 index 3c12811a1..000000000 --- a/src/test/htmlelement/serialize.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import anyTest, { TestInterface } from 'ava'; -import { HydrateableNode, NodeType, SVG_NAMESPACE } from '../../transfer/TransferrableNodes'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { getForTesting as get } from '../../worker-thread/strings'; -import { createDocument, Document } from '../../worker-thread/dom/Document'; -import { HTMLElement } from '../../worker-thread/dom/HTMLElement'; - -const RANDOM_TEXT_CONTENT = `TEXT_CONTENT-${Math.random()}`; -const DIV_ID = 'DIV_ID'; -const DIV_CLASS = 'DIV_CLASS'; - -const test = anyTest as TestInterface<{ - document: Document; - div: HTMLElement; -}>; - -test.beforeEach(t => { - const document = createDocument(); - const div = document.createElement('div'); - - div.setAttribute('id', DIV_ID); - div.setAttribute('class', DIV_CLASS); - div.textContent = RANDOM_TEXT_CONTENT; - - t.context = { - document, - div: div as HTMLElement, - }; -}); - -test('Element should serialize to a TransferrableNode', t => { - const serializedDiv = t.context.div.hydrate(); - t.is(serializedDiv[TransferrableKeys.nodeType], NodeType.ELEMENT_NODE); - t.is(serializedDiv[TransferrableKeys.localOrNodeName], get('div') as number); - - t.not(serializedDiv[TransferrableKeys.childNodes], undefined); - t.is((serializedDiv[TransferrableKeys.childNodes] as Array).length, 1); - t.is((serializedDiv[TransferrableKeys.childNodes] as Array)[0][TransferrableKeys.textContent], get(RANDOM_TEXT_CONTENT) as number); - - t.not(serializedDiv[TransferrableKeys.attributes], undefined); - t.is((serializedDiv[TransferrableKeys.attributes] as Array<[number, number, number]>).length, 2); - t.is((serializedDiv[TransferrableKeys.attributes] as Array<[number, number, number]>)[0][1], get('id') as number); - t.is((serializedDiv[TransferrableKeys.attributes] as Array<[number, number, number]>)[0][2], get(DIV_ID) as number); - t.is((serializedDiv[TransferrableKeys.attributes] as Array<[number, number, number]>)[1][1], get('class') as number); - t.is((serializedDiv[TransferrableKeys.attributes] as Array<[number, number, number]>)[1][2], get(DIV_CLASS) as number); - - // Properties are not yet implemented - // t.is(serializedDiv.properties.length, 0); -}); - -test('Element should serialize namespace', t => { - const { document } = t.context; - const svg = document.createElementNS(SVG_NAMESPACE, 'svg'); - t.is((svg.hydrate() as HydrateableNode)[TransferrableKeys.namespaceURI], get(SVG_NAMESPACE) as number); -}); - -test('Element should serialize child node as well', t => { - const { document } = t.context; - const div = document.createElement('div'); - const childDiv = document.createElement('div'); - div.appendChild(childDiv); - - const serializedDiv = div.hydrate() as HydrateableNode; - const childNodes = serializedDiv[TransferrableKeys.childNodes] || []; - t.is(childNodes.length, 1); - t.deepEqual(childNodes[0], childDiv.hydrate()); -}); diff --git a/src/test/main-thread/helpers/env.ts b/src/test/main-thread/helpers/env.ts index fc34652e6..a4753b6d9 100644 --- a/src/test/main-thread/helpers/env.ts +++ b/src/test/main-thread/helpers/env.ts @@ -63,6 +63,11 @@ export class Env { configurable: true, value: requestAnimationFrame, }); + + Object.defineProperty(global, 'DEBUG_ENABLED', { + configurable: true, + value: false, + }); } dispose() { diff --git a/src/test/main-thread/install.ts b/src/test/main-thread/install.ts index 9484af447..63ca9a631 100644 --- a/src/test/main-thread/install.ts +++ b/src/test/main-thread/install.ts @@ -16,7 +16,6 @@ import anyTest, { TestInterface } from 'ava'; import { Env } from './helpers/env'; -import { WorkerDom } from '../../main-thread/worker-dom'; import { install } from '../../main-thread/install'; const test = anyTest as TestInterface<{ @@ -43,12 +42,15 @@ test.afterEach(t => { test.serial('terminate the worker-dom', t => { const { env, baseElement } = t.context; - const fetchPromise = Promise.all([Promise.resolve('workerDOMScript'), Promise.resolve('authorScript'), Promise.resolve('authorScriptURL')]); - return install(fetchPromise, baseElement).then((workerDom: WorkerDom) => { + const fetchPromise = Promise.all([Promise.resolve('workerDOMScript'), Promise.resolve('authorScript')]); + return install(fetchPromise, baseElement, { + authorURL: 'authorURL', + domURL: 'domURL', + }).then((workerDOM: Worker) => { t.is(env.workers.length, 1); const worker = env.workers[0]; t.is(worker.terminated, false); - workerDom.terminate(); + workerDOM.terminate(); t.is(worker.terminated, true); }); }); diff --git a/src/test/main-thread/long-task.ts b/src/test/main-thread/long-task.ts index 418fdae24..3ad8cecd7 100644 --- a/src/test/main-thread/long-task.ts +++ b/src/test/main-thread/long-task.ts @@ -16,28 +16,37 @@ import anyTest, { TestInterface } from 'ava'; import { Env } from './helpers/env'; -import { LongTaskProcessor } from '../../main-thread/commands/long-task'; +import { LongTaskCommandExecutor, LongTaskExecutor } from '../../main-thread/commands/long-task'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; const test = anyTest as TestInterface<{ env: Env; - processor: LongTaskProcessor; + executor: LongTaskCommandExecutor; longTasks: Array>; + baseElement: HTMLElement; }>; test.beforeEach(t => { const env = new Env(); - + const { document } = env; const longTasks: Array> = []; - const processor = new LongTaskProcessor({ - onLongTask: (promise: Promise) => { + const executor = LongTaskExecutor({ + authorURL: 'authorURL', + domURL: 'domURL', + longTask: (promise: Promise) => { longTasks.push(promise); }, }); + const baseElement = document.createElement('div'); + baseElement._index_ = 1; + document.body.appendChild(baseElement); + t.context = { env, - processor, + executor, longTasks, + baseElement, }; }); @@ -47,74 +56,77 @@ test.afterEach(t => { }); test.serial('should tolerate no callback', t => { - const { longTasks } = t.context; - const processor = new LongTaskProcessor(); + const { longTasks, baseElement } = t.context; + const executor = LongTaskExecutor({ + authorURL: 'authorURL', + domURL: 'domURL', + }); - processor.processStart(); - processor.processEnd(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_START, baseElement._index_]), 0, baseElement); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_END, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 0); }); test.serial('should create and release a long task', t => { - const { processor, longTasks } = t.context; + const { executor, longTasks, baseElement } = t.context; - processor.processStart(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_START, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.true(processor.isInLongTask()); + t.true(executor.active); // Ensure the promise is resolved in the end. - processor.processEnd(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_END, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.false(processor.isInLongTask()); + t.false(executor.active); return longTasks[0]; }); test.serial('should nest long tasks', t => { - const { processor, longTasks } = t.context; + const { executor, longTasks, baseElement } = t.context; - processor.processStart(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_START, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.true(processor.isInLongTask()); + t.true(executor.active); // Nested: no new promise/task created. - processor.processStart(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_START, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.true(processor.isInLongTask()); + t.true(executor.active); // Unnest: the task is still active. - processor.processEnd(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_END, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.true(processor.isInLongTask()); + t.true(executor.active); // Ensure the promise is resolved in the end. - processor.processEnd(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_END, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.false(processor.isInLongTask()); + t.false(executor.active); return longTasks[0]; }); test.serial('should restart a next long tasks', t => { - const { processor, longTasks } = t.context; + const { executor, longTasks, baseElement } = t.context; // Start 1st task. - processor.processStart(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_START, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.true(processor.isInLongTask()); + t.true(executor.active); // End 1st task. - processor.processEnd(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_END, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 1); - t.false(processor.isInLongTask()); + t.false(executor.active); // Start 2nd task. - processor.processStart(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_START, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 2); - t.true(processor.isInLongTask()); + t.true(executor.active); // End 2nd task. - processor.processEnd(); + executor.execute(new Uint16Array([TransferrableMutationType.LONG_TASK_END, baseElement._index_]), 0, baseElement); t.is(longTasks.length, 2); - t.false(processor.isInLongTask()); + t.false(executor.active); // All tasks must resolve. return Promise.all(longTasks) as Promise; diff --git a/src/test/main-thread/mutator.ts b/src/test/main-thread/mutator.ts index 7bf6b7ab9..e95ea9ff5 100644 --- a/src/test/main-thread/mutator.ts +++ b/src/test/main-thread/mutator.ts @@ -17,12 +17,11 @@ import anyTest, { TestInterface } from 'ava'; import { Env } from './helpers/env'; import { MutatorProcessor } from '../../main-thread/mutator'; -import { MutationRecordType } from '../../worker-thread/MutationRecord'; import { NodeContext } from '../../main-thread/nodes'; -import { Phase } from '../../transfer/phase'; import { Strings } from '../../main-thread/strings'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; import { WorkerContext } from '../../main-thread/worker'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { Phase } from '../../transfer/Phase'; const test = anyTest as TestInterface<{ env: Env; @@ -64,33 +63,34 @@ test.afterEach(t => { test.serial('batch mutations', t => { const { env, baseElement, strings, nodeContext, workerContext } = t.context; const { rafTasks } = env; - const mutator = new MutatorProcessor(strings, nodeContext, workerContext); + const mutator = new MutatorProcessor(strings, nodeContext, workerContext, { + domURL: 'domURL', + authorURL: 'authorURL', + }); mutator.mutate( Phase.Mutating, - [], + new ArrayBuffer(0), ['hidden'], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 0, - [TransferrableKeys.value]: 0, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 0, + 0, + 0 + 1, + ]), ); mutator.mutate( Phase.Mutating, - [], + new ArrayBuffer(0), ['data-one'], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 1, - [TransferrableKeys.value]: 1, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 1, + 0, + 1 + 1, + ]), ); t.is(baseElement.getAttribute('hidden'), null); @@ -102,16 +102,15 @@ test.serial('batch mutations', t => { mutator.mutate( Phase.Mutating, - [], + new ArrayBuffer(0), ['data-two'], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 2, - [TransferrableKeys.value]: 2, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 2, + 0, + 2 + 1, + ]), ); t.is(baseElement.getAttribute('data-two'), null); @@ -125,37 +124,37 @@ test.serial('batch mutations with custom pump', t => { const { rafTasks } = env; const tasks: Array<{ phase: Phase; flush: Function }> = []; - const mutationPump = (flush: Function, phase: Phase) => { - tasks.push({ phase, flush }); - }; - - const mutator = new MutatorProcessor(strings, nodeContext, workerContext, { onMutationPump: mutationPump }); + const mutator = new MutatorProcessor(strings, nodeContext, workerContext, { + domURL: 'domURL', + authorURL: 'authorURL', + mutationPump: (flush: Function, phase: Phase) => { + tasks.push({ phase, flush }); + }, + }); mutator.mutate( Phase.Mutating, - [], + new ArrayBuffer(0), ['hidden'], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 0, - [TransferrableKeys.value]: 0, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 0, + 0, + 0 + 1, + ]), ); mutator.mutate( Phase.Mutating, - [], + new ArrayBuffer(0), ['data-one'], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 1, - [TransferrableKeys.value]: 1, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 1, + 0, + 1 + 1, + ]), ); t.is(baseElement.getAttribute('hidden'), null); @@ -169,16 +168,15 @@ test.serial('batch mutations with custom pump', t => { mutator.mutate( Phase.Mutating, - [], + new ArrayBuffer(0), ['data-two'], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 2, - [TransferrableKeys.value]: 2, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 2, + 0, + 2 + 1, + ]), ); t.is(baseElement.getAttribute('data-two'), null); @@ -191,21 +189,23 @@ test.serial('batch mutations with custom pump', t => { test.serial('split strings from mutations', t => { const { env, baseElement, strings, nodeContext, workerContext } = t.context; const { rafTasks } = env; - const mutator = new MutatorProcessor(strings, nodeContext, workerContext); + const mutator = new MutatorProcessor(strings, nodeContext, workerContext, { + domURL: 'domURL', + authorURL: 'authorURL', + }); - mutator.mutate(Phase.Mutating, [], ['hidden'], []); + mutator.mutate(Phase.Mutating, new ArrayBuffer(0), ['hidden'], new Uint16Array([])); mutator.mutate( Phase.Mutating, + new ArrayBuffer(0), [], - [], - [ - { - [TransferrableKeys.type]: MutationRecordType.ATTRIBUTES, - [TransferrableKeys.target]: 2, // Base node. - [TransferrableKeys.attributeName]: 0, - [TransferrableKeys.value]: 0, - }, - ], + new Uint16Array([ + TransferrableMutationType.ATTRIBUTES, + 2, // Base Node + 0, + 0, + 0 + 1, + ]), ); t.is(baseElement.getAttribute('hidden'), null); diff --git a/src/test/main-thread/tsconfig.json b/src/test/main-thread/tsconfig.json index e3df65e85..f89978308 100644 --- a/src/test/main-thread/tsconfig.json +++ b/src/test/main-thread/tsconfig.json @@ -20,4 +20,4 @@ "**/*.ts", "../../main-thread/*.d.ts" ] -} +} \ No newline at end of file diff --git a/src/test/mutation-transfer/appendChild.ts b/src/test/mutation-transfer/appendChild.ts new file mode 100644 index 000000000..3451ec154 --- /dev/null +++ b/src/test/mutation-transfer/appendChild.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Node.appendChild transfers new node', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHILD_LIST, document.body[TransferrableKeys.index], 0, 0, 1, 0, div[TransferrableKeys.index]], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.appendChild(div); + }); +}); + +test.serial.cb('Node.appendChild transfers new node, sibling node', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + div[TransferrableKeys.index], + 1, + 0, + p[TransferrableKeys.index], + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.appendChild(p); + }); +}); + +test.serial.cb('Node.appendChild transfers new node, tree > 1 depth', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHILD_LIST, div[TransferrableKeys.index], 0, 0, 1, 0, p[TransferrableKeys.index]], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.appendChild(p); + }); +}); diff --git a/src/test/mutation-transfer/classListAdd.ts b/src/test/mutation-transfer/classListAdd.ts new file mode 100644 index 000000000..71eacbc42 --- /dev/null +++ b/src/test/mutation-transfer/classListAdd.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.classList.add transfer single value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('bar') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.add('bar'); + }); +}); + +test.serial.cb('Element.classList.add transfer single override value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('foo bar') + 1], + 'mutation is as expected', + ); + t.end(); + } + + div.classList.value = 'foo'; + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.add('bar'); + }); +}); + +test.serial.cb('Element.classList.add transfer multiple values', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('foo bar') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.add('foo', 'bar'); + }); +}); + +test.serial.cb('Element.classList.add mutation observed, multiple value to existing values', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('foo bar baz') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + div.classList.value = 'foo'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.add('bar', 'baz'); + }); +}); diff --git a/src/test/mutation-transfer/classListReplace.ts b/src/test/mutation-transfer/classListReplace.ts new file mode 100644 index 000000000..64430d5f4 --- /dev/null +++ b/src/test/mutation-transfer/classListReplace.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.classList.replace transfer single pre-existing value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('bar') + 1], + 'mutation is as expected', + ); + t.end(); + } + + div.classList.value = 'foo'; + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.replace('foo', 'bar'); + }); +}); + +test.serial.cb('Element.classList.replace transfer multiple pre-existing values', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('bar baz') + 1], + 'mutation is as expected', + ); + t.end(); + } + + div.classList.value = 'foo bar baz'; + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.replace('foo', 'bar'); + }); +}); diff --git a/src/test/mutation-transfer/classListSet.ts b/src/test/mutation-transfer/classListSet.ts new file mode 100644 index 000000000..14f646d7a --- /dev/null +++ b/src/test/mutation-transfer/classListSet.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.classList.set transfer', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('foo bar') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.value = 'foo bar'; + }); +}); diff --git a/src/test/mutation-transfer/classListToggle.ts b/src/test/mutation-transfer/classListToggle.ts new file mode 100644 index 000000000..e0d1081b5 --- /dev/null +++ b/src/test/mutation-transfer/classListToggle.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.classList.toggle transfer remove single value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('') + 1], + 'mutation is as expected', + ); + t.end(); + } + + div.classList.value = 'foo'; + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.toggle('foo'); + }); +}); + +test.serial.cb('Element.classList.toggle mutation observed, toggle to add', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('class'), 0, strings.indexOf('foo bar') + 1], + 'mutation is as expected', + ); + t.end(); + } + + div.classList.value = 'foo'; + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.classList.toggle('bar'); + }); +}); diff --git a/src/test/mutation-transfer/removeAttribute.ts b/src/test/mutation-transfer/removeAttribute.ts new file mode 100644 index 000000000..fc5adc974 --- /dev/null +++ b/src/test/mutation-transfer/removeAttribute.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; +import { HTML_NAMESPACE } from '../../transfer/TransferrableNodes'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.removeAttribute transfer', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('data-foo'), strings.indexOf(HTML_NAMESPACE), 0], + 'mutation is as expected', + ); + t.end(); + } + + div.setAttribute('data-foo', 'foo'); + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.removeAttribute('data-foo'); + }); +}); + +test.serial.cb('Element.removeAttribute transfer, with namespace', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('data-foo'), strings.indexOf('namespace'), 0], + 'mutation is as expected', + ); + t.end(); + } + + div.setAttributeNS('namespace', 'data-foo', 'foo'); + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.removeAttributeNS('namespace', 'data-foo'); + }); +}); diff --git a/src/test/mutation-transfer/removeChild.ts b/src/test/mutation-transfer/removeChild.ts new file mode 100644 index 000000000..0157f4bd5 --- /dev/null +++ b/src/test/mutation-transfer/removeChild.ts @@ -0,0 +1,140 @@ +/** + * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Node.removeChild transfer only child', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHILD_LIST, document.body[TransferrableKeys.index], 0, 0, 0, 1, div[TransferrableKeys.index]], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.removeChild(div); + }); +}); + +test.serial.cb('Node.removeChild transfer, one of siblings', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHILD_LIST, document.body[TransferrableKeys.index], 0, 0, 0, 1, div[TransferrableKeys.index]], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + document.body.appendChild(p); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.removeChild(div); + }); +}); + +test.serial.cb('Node.removeChild transfer, multiple sibling nodes', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + const input = document.createElement('input'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + 0, + 0, + 1, + div[TransferrableKeys.index], + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + 0, + 0, + 1, + input[TransferrableKeys.index], + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + document.body.appendChild(p); + document.body.appendChild(input); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.removeChild(div); + document.body.removeChild(input); + }); +}); + +test.serial.cb('Node.removeChild transfer, tree > 1 depth', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHILD_LIST, div[TransferrableKeys.index], 0, 0, 0, 1, p[TransferrableKeys.index]], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + div.appendChild(p); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.removeChild(p); + }); +}); diff --git a/src/test/mutation-transfer/replaceChild.ts b/src/test/mutation-transfer/replaceChild.ts new file mode 100644 index 000000000..02b351914 --- /dev/null +++ b/src/test/mutation-transfer/replaceChild.ts @@ -0,0 +1,168 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Node.replaceChild transfer only node', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + 0, + 1, + 1, + p[TransferrableKeys.index], + div[TransferrableKeys.index], + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.replaceChild(p, div); + }); +}); + +test.serial.cb('Node.replaceChild transfer replace first with second', t => { + const { document, emitter } = t.context; + const first = document.createElement('first'); + const second = document.createElement('second'); + const third = document.createElement('third'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + third[TransferrableKeys.index], + 0, + 1, + 1, + second[TransferrableKeys.index], + first[TransferrableKeys.index], + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(first); + document.body.appendChild(third); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.replaceChild(second, first); + }); +}); + +test.serial.cb('Node.replaceChild transfer replace third with second', t => { + const { document, emitter } = t.context; + const first = document.createElement('first'); + const second = document.createElement('second'); + const third = document.createElement('third'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + 0, + 1, + 1, + second[TransferrableKeys.index], + third[TransferrableKeys.index], + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(first); + document.body.appendChild(third); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.replaceChild(second, third); + }); +}); + +test.serial.cb('Node.replaceChild transfer remove sibling node', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + const p = document.createElement('p'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + 0, + 0, + 1, + div[TransferrableKeys.index], + TransferrableMutationType.CHILD_LIST, + document.body[TransferrableKeys.index], + 0, + 0, + 1, + 1, + div[TransferrableKeys.index], + p[TransferrableKeys.index], + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + document.body.appendChild(p); + Promise.resolve().then(() => { + emitter.once(transmitted); + document.body.replaceChild(div, p); + }); +}); diff --git a/src/test/mutation-transfer/setAttribute.ts b/src/test/mutation-transfer/setAttribute.ts new file mode 100644 index 000000000..e12b7dec6 --- /dev/null +++ b/src/test/mutation-transfer/setAttribute.ts @@ -0,0 +1,139 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { HTML_NAMESPACE } from '../../transfer/TransferrableNodes'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.setAttribute transfers new attribute', t => { + const { document, emitter } = t.context; + const el = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + el[TransferrableKeys.index], + strings.indexOf('data-foo'), + strings.indexOf(HTML_NAMESPACE), + strings.indexOf('bar') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + el.setAttribute('data-foo', 'bar'); + }); +}); + +test.serial.cb('Element.setAttribute transfers attribute overwrite', t => { + const { document, emitter } = t.context; + const el = document.createElement('div'); + el.setAttribute('data-foo', 'bar'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + el[TransferrableKeys.index], + strings.indexOf('data-foo'), + strings.indexOf(HTML_NAMESPACE), + strings.indexOf('baz') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + el.setAttribute('data-foo', 'baz'); + }); +}); + +test.serial.cb('Element.setAttribute transfers new attribute with namespace', t => { + const { document, emitter } = t.context; + const el = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + el[TransferrableKeys.index], + strings.indexOf('data-foo'), + strings.indexOf('namespace'), + strings.indexOf('bar') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + el.setAttributeNS('namespace', 'data-foo', 'bar'); + }); +}); + +test.serial.cb('Element.setAttribute transfers attribute overwrite with namespace', t => { + const { document, emitter } = t.context; + const el = document.createElement('div'); + el.setAttributeNS('namespace', 'data-foo', 'bar'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + el[TransferrableKeys.index], + strings.indexOf('data-foo'), + strings.indexOf('namespace'), + strings.indexOf('baz') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + el.setAttributeNS('namespace', 'data-foo', 'baz'); + }); +}); diff --git a/src/test/mutation-transfer/setCharacterData.ts b/src/test/mutation-transfer/setCharacterData.ts new file mode 100644 index 000000000..3a1d17462 --- /dev/null +++ b/src/test/mutation-transfer/setCharacterData.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Text, set data', t => { + const { document, emitter } = t.context; + const text = document.createTextNode('original text'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHARACTER_DATA, text[TransferrableKeys.index], strings.indexOf('new text')], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + text.data = 'new text'; + }); +}); + +test.serial.cb('Text, set textContent', t => { + const { document, emitter } = t.context; + const text = document.createTextNode('original text'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.CHARACTER_DATA, text[TransferrableKeys.index], strings.indexOf('new text')], + 'mutation is as expected', + ); + t.end(); + } + + Promise.resolve().then(() => { + emitter.once(transmitted); + text.textContent = 'new text'; + }); +}); diff --git a/src/test/mutation-transfer/styleAdd.ts b/src/test/mutation-transfer/styleAdd.ts new file mode 100644 index 000000000..5eb58d862 --- /dev/null +++ b/src/test/mutation-transfer/styleAdd.ts @@ -0,0 +1,183 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; +import { appendKeys } from '../../worker-thread/css/CSSStyleDeclaration'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.style transfer single value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('width: 10px;') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.width = '10px'; + }); +}); + +test.serial.cb('Element.style transfer multiple values', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + div[TransferrableKeys.index], + strings.indexOf('style'), + 0, + strings.indexOf('width: 10px; height: 12px;') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width', 'height']); + div.style.width = '10px'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.height = '12px'; + }); +}); + +test.serial.cb('Element.style transfer single value, via setProperty', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('width: 10px;') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.setProperty('width', '10px'); + }); +}); + +test.serial.cb('Element.style transfer multiple values, via setProperty', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + div[TransferrableKeys.index], + strings.indexOf('style'), + 0, + strings.indexOf('width: 10px; height: 12px;') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width', 'height']); + div.style.setProperty('width', '10px'); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.setProperty('height', '12px'); + }); +}); + +test.serial.cb('Element.style transfer single value, via cssText', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('width: 10px;') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.cssText = 'width: 10px'; + }); +}); + +test.serial.cb('Element.style transfer multiple values, via cssText', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + div[TransferrableKeys.index], + strings.indexOf('style'), + 0, + strings.indexOf('width: 10px; height: 12px;') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width', 'height']); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.cssText = 'width: 10px; height: 12px'; + }); +}); diff --git a/src/test/mutation-transfer/styleRemove.ts b/src/test/mutation-transfer/styleRemove.ts new file mode 100644 index 000000000..32acb1948 --- /dev/null +++ b/src/test/mutation-transfer/styleRemove.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; +import { appendKeys } from '../../worker-thread/css/CSSStyleDeclaration'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.style transfer single value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + div.style.width = '10px'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.width = ''; + }); +}); + +test.serial.cb('Element.style transfer single value, via setProperty', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + div.style.setProperty('width', '10px'); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.setProperty('width', ''); + }); +}); + +test.serial.cb('Element.style transfer single value, via removeProperty', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + div.style.setProperty('width', '10px'); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.removeProperty('width'); + }); +}); + +test.cb('Element.style transfer single value, via cssText', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + div.style.cssText = 'width: 10px'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.cssText = ''; + }); +}); diff --git a/src/test/mutation-transfer/styleReplace.ts b/src/test/mutation-transfer/styleReplace.ts new file mode 100644 index 000000000..5e279c5a7 --- /dev/null +++ b/src/test/mutation-transfer/styleReplace.ts @@ -0,0 +1,138 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import anyTest, { TestInterface } from 'ava'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { MutationFromWorker } from '../../transfer/Messages'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { emitter, Emitter } from '../Emitter'; +import { appendKeys } from '../../worker-thread/css/CSSStyleDeclaration'; + +const test = anyTest as TestInterface<{ + document: Document; + emitter: Emitter; +}>; + +test.beforeEach(t => { + const document = createDocument(); + + t.context = { + document, + emitter: emitter(document), + }; +}); + +test.serial.cb('Element.style transfer single value', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('width: 12px;') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + div.style.width = '10px'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.width = '12px'; + }); +}); + +test.serial.cb('Element.style transfer multiple values', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + div[TransferrableKeys.index], + strings.indexOf('style'), + 0, + strings.indexOf('width: 14px; height: 12px;') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width', 'height']); + div.style.width = '10px'; + div.style.height = '12px'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.width = '14px'; + }); +}); + +test.serial.cb('Element.style transfer single value, setProperty', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [TransferrableMutationType.ATTRIBUTES, div[TransferrableKeys.index], strings.indexOf('style'), 0, strings.indexOf('width: 12px;') + 1], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width']); + div.style.setProperty('width', '10px'); + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.setProperty('width', '12px'); + }); +}); + +test.serial.cb('Element.style.width mutation observed, multiple values, via cssText', t => { + const { document, emitter } = t.context; + const div = document.createElement('div'); + + function transmitted(strings: Array, message: MutationFromWorker, buffers: Array) { + t.deepEqual( + Array.from(new Uint16Array(message[TransferrableKeys.mutations])), + [ + TransferrableMutationType.ATTRIBUTES, + div[TransferrableKeys.index], + strings.indexOf('style'), + 0, + strings.indexOf('width: 12px; height: 14px;') + 1, + ], + 'mutation is as expected', + ); + t.end(); + } + + document.body.appendChild(div); + appendKeys(['width', 'height']); + div.style.cssText = 'width: 10px; height: 12px'; + Promise.resolve().then(() => { + emitter.once(transmitted); + div.style.cssText = 'width: 12px; height: 14px'; + }); +}); diff --git a/src/test/mutationobserver/addEventListener.ts b/src/test/mutationobserver/addEventListener.ts deleted file mode 100644 index 2e7f77176..000000000 --- a/src/test/mutationobserver/addEventListener.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import anyTest, { TestInterface } from 'ava'; -import { createDocument, Document } from '../../worker-thread/dom/Document'; -import { Element } from '../../worker-thread/dom/Element'; -import { MutationRecord, MutationRecordType } from '../../worker-thread/MutationRecord'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { getForTesting as get } from '../../worker-thread/strings'; - -const test = anyTest as TestInterface<{ - document: Document; - el: Element; - callback: () => undefined; -}>; - -test.beforeEach(t => { - const document = createDocument(); - - t.context = { - document, - el: document.createElement('div'), - callback: () => undefined, - }; -}); -test.afterEach(t => { - t.context.document.body.childNodes.forEach(childNode => childNode.remove()); -}); - -test.serial.cb('Element.addEventListener mutation observed when node is connected.', t => { - const { document, el, callback } = t.context; - const observer = new document.defaultView.MutationObserver( - (mutations: MutationRecord[]): void => { - t.deepEqual(mutations, [ - { - type: MutationRecordType.EVENT_SUBSCRIPTION, - target: el, - addedEvents: [ - { - [TransferrableKeys.type]: get('mouseenter') as number, - [TransferrableKeys.index]: 3, - [TransferrableKeys.index]: 0, - }, - ], - }, - ]); - observer.disconnect(); - t.end(); - }, - ); - - document.body.appendChild(el); - observer.observe(document.body); - el.addEventListener('mouseenter', callback); -}); - -test.serial.cb('Element.addEventListener mutation observed when node is not yet connected.', t => { - const { document, el, callback } = t.context; - const observer = new document.defaultView.MutationObserver( - (mutations: MutationRecord[]): void => { - t.deepEqual(mutations, [ - { - type: MutationRecordType.EVENT_SUBSCRIPTION, - target: el, - addedEvents: [ - { - [TransferrableKeys.type]: get('mouseenter') as number, - [TransferrableKeys.index]: 4, - [TransferrableKeys.index]: 0, - }, - ], - }, - ]); - observer.disconnect(); - t.end(); - }, - ); - - observer.observe(document.body); - el.addEventListener('mouseenter', callback); -}); diff --git a/src/test/mutationobserver/longTask.ts b/src/test/mutationobserver/longTask.ts deleted file mode 100644 index 3c5c99584..000000000 --- a/src/test/mutationobserver/longTask.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright 2019 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import anyTest, { TestInterface } from 'ava'; -import { createDocument, Document } from '../../worker-thread/dom/Document'; -import { wrap as longTaskWrap } from '../../worker-thread/long-task'; -import { MutationRecord, MutationRecordType } from '../../worker-thread/MutationRecord'; - -const test = anyTest as TestInterface<{ - document: Document; -}>; - -test.beforeEach(t => { - const document = createDocument(); - - t.context = { - document, - }; -}); - -test.serial('execute a long task via wrapper', t => { - const { document } = t.context; - - const callback = function() { - return Promise.resolve(-1); - }; - - let startResolver: Function; - let endResolver: Function; - const startPromise = new Promise(resolve => { - startResolver = resolve; - }); - const endPromise = new Promise(resolve => { - endResolver = resolve; - }); - - const observer = new document.defaultView.MutationObserver( - (mutations: MutationRecord[]): void => { - for (const mutation of mutations) { - if (mutation.type == MutationRecordType.LONG_TASK_START) { - startResolver(); - } - if (mutation.type == MutationRecordType.LONG_TASK_END) { - endResolver(); - } - } - }, - ); - observer.observe(document); - - return longTaskWrap(document, callback)() - .then((result: any) => { - t.is(result, -1); - return Promise.all([startPromise, endPromise]) as Promise; - }) - .then(() => { - observer.disconnect(); - }); -}); diff --git a/src/test/mutationobserver/removeEventListener.ts b/src/test/mutationobserver/removeEventListener.ts deleted file mode 100644 index 7dd43aafb..000000000 --- a/src/test/mutationobserver/removeEventListener.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import anyTest, { TestInterface } from 'ava'; -import { createDocument, Document } from '../../worker-thread/dom/Document'; -import { Element } from '../../worker-thread/dom/Element'; -import { MutationRecord, MutationRecordType } from '../../worker-thread/MutationRecord'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { getForTesting as get } from '../../worker-thread/strings'; - -const test = anyTest as TestInterface<{ - document: Document; - el: Element; - callback: () => undefined; -}>; - -test.beforeEach(t => { - const document = createDocument(); - - t.context = { - document, - el: document.createElement('div'), - callback: () => undefined, - }; -}); - -test.serial.cb('Element.removeEventListener mutation observed when node is connected.', t => { - const { document, el, callback } = t.context; - const observer = new document.defaultView.MutationObserver( - (mutations: MutationRecord[]): void => { - t.deepEqual(mutations, [ - { - type: MutationRecordType.EVENT_SUBSCRIPTION, - target: el, - removedEvents: [ - { - [TransferrableKeys.type]: get('mouseenter') as number, - [TransferrableKeys.index]: 3, - [TransferrableKeys.index]: 0, - }, - ], - }, - ]); - observer.disconnect(); - t.end(); - }, - ); - - document.body.appendChild(el); - el.addEventListener('mouseenter', callback); - observer.observe(document.body); - el.removeEventListener('mouseenter', callback); -}); - -test.serial.cb('Element.removeEventListener mutation observed when node is not yet connected.', t => { - const { document, el, callback } = t.context; - const observer = new document.defaultView.MutationObserver( - (mutations: MutationRecord[]): void => { - t.deepEqual(mutations, [ - { - type: MutationRecordType.EVENT_SUBSCRIPTION, - target: el, - removedEvents: [ - { - [TransferrableKeys.type]: get('mouseenter') as number, - [TransferrableKeys.index]: 4, - [TransferrableKeys.index]: 0, - }, - ], - }, - ]); - observer.disconnect(); - t.end(); - }, - ); - - el.addEventListener('mouseenter', callback); - observer.observe(document.body); - el.removeEventListener('mouseenter', callback); -}); diff --git a/src/test/mutationobserver/replaceChild.ts b/src/test/mutationobserver/replaceChild.ts index df6e840c8..06099e7b4 100644 --- a/src/test/mutationobserver/replaceChild.ts +++ b/src/test/mutationobserver/replaceChild.ts @@ -116,20 +116,20 @@ test.serial.cb('replaceChild mutation, remove sibling node', t => { const observer = new document.defaultView.MutationObserver( (mutations: MutationRecord[]): void => { t.is(mutations.length, 2); - - // Mutation for removeChild(div). - const one = mutations[0]; - t.is(one.type, MutationRecordType.CHILD_LIST); - t.is(one.target, document.body); - t.deepEqual(one.removedNodes, [div]); - - // Mutation for replaceChild(div, p). - const two = mutations[1]; - t.is(two.type, MutationRecordType.CHILD_LIST); - t.is(two.target, document.body); - t.deepEqual(two.removedNodes, [p]); // [div] - t.deepEqual(two.addedNodes, [div]); // [] - t.is(two.nextSibling, undefined); + t.deepEqual(mutations, [ + { + type: MutationRecordType.CHILD_LIST, + target: document.body, + removedNodes: [div], + }, + { + type: MutationRecordType.CHILD_LIST, + target: document.body, + removedNodes: [p], + addedNodes: [div], + nextSibling: undefined, + }, + ]); observer.disconnect(); t.end(); }, diff --git a/src/test/node/textContent.ts b/src/test/node/textContent.ts index ae12a0a35..cc8adcf57 100644 --- a/src/test/node/textContent.ts +++ b/src/test/node/textContent.ts @@ -17,9 +17,11 @@ import anyTest, { TestInterface } from 'ava'; import { Text } from '../../worker-thread/dom/Text'; import { Element } from '../../worker-thread/dom/Element'; -import { createDocument } from '../../worker-thread/dom/Document'; +import { createDocument, Document } from '../../worker-thread/dom/Document'; +import { NodeType } from '../../transfer/TransferrableNodes'; const test = anyTest as TestInterface<{ + document: Document; node: Element; child: Element; nodeText: Text; @@ -30,6 +32,7 @@ test.beforeEach(t => { const document = createDocument(); t.context = { + document, node: document.createElement('div'), child: document.createElement('p'), nodeText: document.createTextNode('text in node'), @@ -69,3 +72,30 @@ test('textContent returns the value of all depths childNodes when there are chil t.is(node.textContent, nodeText.textContent + childText.textContent); t.is(node.textContent, 'text in node text in child'); }); + +test('textContent setter removes other child element nodes', t => { + const { node, child } = t.context; + child.textContent = 'foo'; + node.appendChild(child); + + t.is(node.childNodes.length, 1); + t.is(node.childNodes[0].nodeType, NodeType.ELEMENT_NODE); + t.is(node.textContent, 'foo'); + node.textContent = 'bar'; + t.is(node.textContent, 'bar'); + t.is(node.childNodes.length, 1); + t.is(node.childNodes[0].nodeType, NodeType.TEXT_NODE); +}); + +test('textContent setter removes other child text nodes', t => { + const { node, document } = t.context; + node.appendChild(document.createTextNode('f')); + node.appendChild(document.createTextNode('o')); + node.appendChild(document.createTextNode('o')); + + t.is(node.textContent, 'foo'); + t.is(node.childNodes.length, 3); + node.textContent = ''; + t.is(node.textContent, ''); + t.is(node.childNodes.length, 1); +}); diff --git a/src/third_party/html-parser/html-parser.ts b/src/third_party/html-parser/html-parser.ts index 670f6a10f..0001e4091 100644 --- a/src/third_party/html-parser/html-parser.ts +++ b/src/third_party/html-parser/html-parser.ts @@ -33,18 +33,42 @@ const kSelfClosingElements: Elements = { PARAM: true, SOURCE: true, TRACK: true, - WBR: true + WBR: true, }; const kElementsClosedByOpening: ElementMapping = { LI: { LI: true }, DT: { DT: true, DD: true }, DD: { DD: true, DT: true }, - P: { ADDRESS: true, ARTICLE: true, ASIDE: true, BLOCKQUOTE: true, - DETAILS: true, DIV: true, DL: true, FIELDSET: true, FIGCAPTION: true, - FIGURE: true, FOOTER: true, FORM: true, H1: true, H2: true, H3: true, - H4: true, H5: true, H6: true, HEADER: true, HR: true, MAIN: true, - NAV: true, OL: true, P: true, PRE: true, SECTION: true, TABLE: true, - UL: true }, + P: { + ADDRESS: true, + ARTICLE: true, + ASIDE: true, + BLOCKQUOTE: true, + DETAILS: true, + DIV: true, + DL: true, + FIELDSET: true, + FIGCAPTION: true, + FIGURE: true, + FOOTER: true, + FORM: true, + H1: true, + H2: true, + H3: true, + H4: true, + H5: true, + H6: true, + HEADER: true, + HR: true, + MAIN: true, + NAV: true, + OL: true, + P: true, + PRE: true, + SECTION: true, + TABLE: true, + UL: true, + }, RT: { RT: true, RP: true }, RP: { RT: true, RP: true }, OPTGROUP: { OPTGROUP: true }, @@ -80,8 +104,7 @@ const kBlockTextElements: Elements = { */ export function parse(data: string, rootElement: Element) { const ownerDocument = rootElement.ownerDocument; - const root = - ownerDocument.createElementNS(rootElement.namespaceURI, rootElement.localName); + const root = ownerDocument.createElementNS(rootElement.namespaceURI, rootElement.localName); let currentParent = root as Node; let currentNamespace = root.namespaceURI; @@ -92,11 +115,10 @@ export function parse(data: string, rootElement: Element) { const tagsClosed = [] as string[]; if (currentNamespace !== SVG_NAMESPACE && currentNamespace !== HTML_NAMESPACE) { - throw new Error("Namespace not supported: " + currentNamespace); + throw new Error('Namespace not supported: ' + currentNamespace); } while ((match = kMarkupPattern.exec(data))) { - const commentContents = match[1]; // let beginningSlash = match[2]; // ... or
etc. while (true) { @@ -204,7 +225,7 @@ export function parse(data: string, rootElement: Element) { if (wrapper) { wrapper.parentNode = null; wrapper.childNodes.forEach((node: Node) => { - node.parentNode = null; + node.parentNode = null; }); return wrapper; } diff --git a/src/transfer/Messages.ts b/src/transfer/Messages.ts index 064f93bcb..9addedbb6 100644 --- a/src/transfer/Messages.ts +++ b/src/transfer/Messages.ts @@ -15,11 +15,11 @@ */ import { TransferrableEvent } from './TransferrableEvent'; -import { TransferrableMutationRecord } from './TransferrableRecord'; import { TransferrableSyncValue } from './TransferrableSyncValue'; import { TransferrableKeys } from './TransferrableKeys'; -import { TransferrableNode, HydrateableNode, TransferredNode } from './TransferrableNodes'; -import { TransferrableBoundingClientRect } from './TransferrableCommands'; +import { HydrateableNode, TransferredNode } from './TransferrableNodes'; +import { TransferrableBoundingClientRect } from './TransferrableBoundClientRect'; +import { Phase } from './Phase'; export const enum MessageType { // INIT = 0, @@ -37,9 +37,10 @@ export const enum MessageType { export interface MutationFromWorker { readonly [TransferrableKeys.type]: MessageType; + readonly [TransferrableKeys.phase]: Phase; readonly [TransferrableKeys.strings]: Array; - readonly [TransferrableKeys.nodes]: Array; - readonly [TransferrableKeys.mutations]: Array; + readonly [TransferrableKeys.nodes]: ArrayBuffer; + readonly [TransferrableKeys.mutations]: ArrayBuffer; } export type MessageFromWorker = { data: MutationFromWorker; @@ -63,10 +64,4 @@ export interface BoundingClientRectToWorker { [TransferrableKeys.target]: TransferredNode; [TransferrableKeys.data]: TransferrableBoundingClientRect; } -export interface LongTaskStartToWorker { - [TransferrableKeys.type]: MessageType.LONG_TASK_START; -} -export interface LongTaskEndToWorker { - [TransferrableKeys.type]: MessageType.LONG_TASK_END; -} -export type MessageToWorker = EventToWorker | ValueSyncToWorker | BoundingClientRectToWorker | LongTaskStartToWorker | LongTaskEndToWorker; +export type MessageToWorker = EventToWorker | ValueSyncToWorker | BoundingClientRectToWorker; diff --git a/src/transfer/phase.ts b/src/transfer/Phase.ts similarity index 100% rename from src/transfer/phase.ts rename to src/transfer/Phase.ts diff --git a/src/transfer/TransferrableCommands.ts b/src/transfer/TransferrableBoundClientRect.ts similarity index 83% rename from src/transfer/TransferrableCommands.ts rename to src/transfer/TransferrableBoundClientRect.ts index fbd05bed5..dd1aebda6 100644 --- a/src/transfer/TransferrableCommands.ts +++ b/src/transfer/TransferrableBoundClientRect.ts @@ -1,5 +1,5 @@ /** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,10 @@ * limitations under the License. */ +export const enum BoundClientRectMutationIndex { + Target = 1, + End = 2, +} + // [top, right, bottom, left, width, height] export type TransferrableBoundingClientRect = [number, number, number, number, number, number]; diff --git a/src/transfer/TransferrableEvent.ts b/src/transfer/TransferrableEvent.ts index f692f9f09..58eb8174b 100644 --- a/src/transfer/TransferrableEvent.ts +++ b/src/transfer/TransferrableEvent.ts @@ -1,5 +1,5 @@ /** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,23 @@ export interface TransferrableEvent { readonly [TransferrableKeys.keyCode]?: number; } -export interface TransferrableEventSubscriptionChange { - readonly [TransferrableKeys.type]: number; - readonly [TransferrableKeys.index]: number; - readonly [TransferrableKeys.index]: number; +/** + * Event Subscription Transfer + * + * [ + * TransferrableMutationType.EVENT_SUBSCRIPTION, + * Target.index, + * RemoveEventListener.count, + * AddEventListener.count, + * ...RemoveEvent<[ EventRegistration.type, EventRegistration.index ]>, + * ...AddEvent<[ EventRegistration.type, EventRegistration.index ]>, + * ] + */ +export const enum EventSubscriptionMutationIndex { + Target = 1, + RemoveEventListenerCount = 2, + AddEventListenerCount = 3, + Events = 4, + End = 4, } +export const EVENT_SUBSCRIPTION_LENGTH = 2; diff --git a/src/transfer/TransferrableKeys.ts b/src/transfer/TransferrableKeys.ts index ae93204ee..16215d533 100644 --- a/src/transfer/TransferrableKeys.ts +++ b/src/transfer/TransferrableKeys.ts @@ -69,4 +69,6 @@ export const enum TransferrableKeys { end = 51, selected = 52, command = 53, + phase = 54, + worker = 55, } diff --git a/src/transfer/TransferrableMutation.ts b/src/transfer/TransferrableMutation.ts new file mode 100644 index 000000000..58ecee27e --- /dev/null +++ b/src/transfer/TransferrableMutation.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const enum TransferrableMutationType { + ATTRIBUTES = 0, + CHARACTER_DATA = 1, + CHILD_LIST = 2, + PROPERTIES = 3, + EVENT_SUBSCRIPTION = 4, + GET_BOUNDING_CLIENT_RECT = 5, + LONG_TASK_START = 6, + LONG_TASK_END = 7, +} + +export const ReadableMutationType: { [key: number]: string } = { + 0: 'ATTRIBUTES', + 1: 'CHARACTER_DATA', + 2: 'CHILD_LIST', + 3: 'PROPERTIES', + 4: 'EVENT_SUBSCRIPTION', + 5: 'GET_BOUNDING_CLIENT_RECT', + 6: 'LONG_TASK_START', + 7: 'LONG_TASK_END', +}; + +/** + * Child List Mutations + * [ + * TransferrableMutationType.CHILD_LIST, + * Target.index, + * NextSibling.index, + * PreviousSibling.index, + * AppendedNodeCount, + * RemovedNodeCount, + * ... AppendedNode.index, + * ... RemovedNode.index, + * ] + */ +export const enum ChildListMutationIndex { + Target = 1, + NextSibling = 2, + PreviousSibling = 3, + AppendedNodeCount = 4, + RemovedNodeCount = 5, + Nodes = 6, + End = 6, +} + +/** + * Attribute Mutations + * [ + * TransferrableMutationType.ATTRIBUTES, + * Target.index, + * Attr.name, + * Attr.namespace, // 0 is the default value. + * Attr.value, // 0 is the default value. + * ] + */ +export const enum AttributeMutationIndex { + Target = 1, + Name = 2, + Namespace = 3, + Value = 4, + End = 5, +} + +/** + * Character Data Mutations + * [ + * TransferrableMutationType.CHARACTER_DATA, + * Target.index, + * CharacterData.value, + * ] + */ +export const enum CharacterDataMutationIndex { + Target = 1, + Value = 2, + End = 3, +} + +/** + * Properties Mutations + * [ + * TransferrableMutationType.PROPERTIES, + * Target.index, + * Property.name, + * Property.value, + * ] + */ +export const enum PropertyMutationIndex { + Target = 1, + Name = 2, + Value = 3, + End = 4, +} + +/** + * Long Task Mutations + * [ + * TransferrableMutationType.LONG_TASK_START || TransferrableMutation.LONG_TASK_END + * Target.index, + * ] + */ +export const enum LongTaskMutationIndex { + Target = 1, + End = 2, +} diff --git a/src/transfer/TransferrableNodes.ts b/src/transfer/TransferrableNodes.ts index d182f2a96..22f8be18a 100644 --- a/src/transfer/TransferrableNodes.ts +++ b/src/transfer/TransferrableNodes.ts @@ -17,6 +17,8 @@ import { NumericBoolean } from '../utils'; import { TransferrableKeys } from './TransferrableKeys'; +export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; +export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; export const enum NodeType { ELEMENT_NODE = 1, ATTRIBUTE_NODE = 2, @@ -32,26 +34,48 @@ export const enum NodeType { NOTATION_NODE = 12, } -export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; -export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; - -export interface HydrateableNode extends TransferrableNode { +export interface HydrateableNode { + readonly [TransferrableKeys.index]: number; + readonly [TransferrableKeys.transferred]: NumericBoolean; + readonly [TransferrableKeys.nodeType]: NodeType; + readonly [TransferrableKeys.localOrNodeName]: number; + [TransferrableKeys.textContent]?: number; + [TransferrableKeys.namespaceURI]?: number; [TransferrableKeys.attributes]?: Array<[number, number, number]>; [TransferrableKeys.childNodes]?: Array; } +// If a Node has been transferred once already to main thread then we need only pass its index. +export interface TransferredNode { + readonly [0]: number; +} + +export const enum TransferredNodeIndex { + Index = 0, +} + +type TransferrableNodeName = number; +type TransferrableTextContent = number; +type TransferrableNamespaceURI = number; export interface TransferrableNode extends TransferredNode { - readonly [TransferrableKeys.nodeType]: NodeType; - readonly [TransferrableKeys.localOrNodeName]: number; + readonly [1]: NodeType; + readonly [2]: TransferrableNodeName; // Optional keys that are defined at construction of a `Text` or `Element`. // This makes the keys observed. - [TransferrableKeys.textContent]?: number; - [TransferrableKeys.namespaceURI]?: number; + readonly [3]: NumericBoolean; + readonly [4]: TransferrableTextContent; + readonly [5]: NumericBoolean; + readonly [6]: TransferrableNamespaceURI; } -// If a Node has been transferred once already to main thread then we need only pass its index. -export interface TransferredNode { - readonly [TransferrableKeys.index]: number; - readonly [TransferrableKeys.transferred]: NumericBoolean; +export const enum TransferrableNodeIndex { + Index = 0, + NodeType = 1, + NodeName = 2, + ContainsText = 3, + TextContent = 4, + ContainsNamespace = 5, + Namespace = 6, + End = 7, } diff --git a/src/transfer/TransferrableRecord.ts b/src/transfer/TransferrableRecord.ts deleted file mode 100644 index 40fcc73f9..000000000 --- a/src/transfer/TransferrableRecord.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MutationRecordType } from '../worker-thread/MutationRecord'; -import { TransferredNode } from './TransferrableNodes'; -import { TransferrableKeys } from './TransferrableKeys'; -import { TransferrableEventSubscriptionChange } from './TransferrableEvent'; - -// The TransferrableMutationRecord interface is modification and extension of -// the real MutationRecord, with changes to support the transferring of -// Mutations across threads and for properties (not currently supported by MutationRecord). - -// For more info on MutationRecords: https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord -export interface TransferrableMutationRecord { - readonly [TransferrableKeys.type]: MutationRecordType; - readonly [TransferrableKeys.target]: number; - - [TransferrableKeys.addedNodes]?: Array; - [TransferrableKeys.removedNodes]?: Array; - [TransferrableKeys.previousSibling]?: TransferredNode; - [TransferrableKeys.nextSibling]?: TransferredNode; - [TransferrableKeys.attributeName]?: number; - [TransferrableKeys.attributeNamespace]?: number; - [TransferrableKeys.propertyName]?: number; - [TransferrableKeys.value]?: number; - [TransferrableKeys.oldValue]?: number; - [TransferrableKeys.addedEvents]?: Array; - [TransferrableKeys.removedEvents]?: Array; -} diff --git a/src/worker-thread/DocumentMutations.ts b/src/worker-thread/DocumentMutations.ts deleted file mode 100644 index ddc0f033f..000000000 --- a/src/worker-thread/DocumentMutations.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Node } from './dom/Node'; -import { Document } from './dom/Document'; -import { MutationRecord } from './MutationRecord'; -import { Phase } from '../transfer/phase'; -import { TransferrableMutationRecord } from '../transfer/TransferrableRecord'; -import { TransferrableNode, TransferredNode } from '../transfer/TransferrableNodes'; -import { MessageType, MutationFromWorker } from '../transfer/Messages'; -import { TransferrableKeys } from '../transfer/TransferrableKeys'; -import { consume as consumeNodes } from './nodes'; -import { store as storeString, consume as consumeStrings } from './strings'; -import { phase, set as setPhase } from './phase'; - -let observing = false; - -const serializeNodes = (nodes: Array): Array => nodes.map(node => node[TransferrableKeys.transferredFormat]); - -/** - * - * @param mutations - */ -function serializeMutations(mutations: MutationRecord[]): MutationFromWorker { - const nodes: Array = consumeNodes().map(node => node[TransferrableKeys.creationFormat]); - const transferrableMutations: TransferrableMutationRecord[] = []; - const type = phase === Phase.Mutating ? MessageType.MUTATE : MessageType.HYDRATE; - - mutations.forEach(mutation => { - let transferable: TransferrableMutationRecord = { - [TransferrableKeys.type]: mutation.type, - [TransferrableKeys.target]: mutation.target[TransferrableKeys.index], - }; - - mutation.addedNodes && (transferable[TransferrableKeys.addedNodes] = serializeNodes(mutation.addedNodes)); - mutation.removedNodes && (transferable[TransferrableKeys.removedNodes] = serializeNodes(mutation.removedNodes)); - mutation.nextSibling && (transferable[TransferrableKeys.nextSibling] = mutation.nextSibling[TransferrableKeys.transferredFormat]); - mutation.attributeName != null && (transferable[TransferrableKeys.attributeName] = storeString(mutation.attributeName)); - mutation.attributeNamespace != null && (transferable[TransferrableKeys.attributeNamespace] = storeString(mutation.attributeNamespace)); - mutation.oldValue != null && (transferable[TransferrableKeys.oldValue] = storeString(mutation.oldValue)); - mutation.propertyName && (transferable[TransferrableKeys.propertyName] = storeString(mutation.propertyName)); - mutation.value != null && (transferable[TransferrableKeys.value] = storeString(mutation.value)); - mutation.addedEvents && (transferable[TransferrableKeys.addedEvents] = mutation.addedEvents); - mutation.removedEvents && (transferable[TransferrableKeys.removedEvents] = mutation.removedEvents); - - transferrableMutations.push(transferable); - }); - return { - [TransferrableKeys.type]: type, - [TransferrableKeys.strings]: consumeStrings(), - [TransferrableKeys.nodes]: nodes, - [TransferrableKeys.mutations]: transferrableMutations, - }; -} - -/** - * - * @param incoming - * @param postMessage - */ -function handleMutations(incoming: Array, postMessage?: Function): void { - if (postMessage) { - postMessage(serializeMutations(incoming)); - // Only first set of mutations are sent in a "HYDRATE" message type. - // Afterwards, we enter "MUTATING" phase and subsequent mutations are sent in "MUTATE" message type. - setPhase(Phase.Mutating); - } -} - -/** - * - * @param doc - * @param postMessage - */ -export function observe(doc: Document, postMessage: Function): void { - if (!observing) { - new doc.defaultView.MutationObserver(mutations => handleMutations(mutations, postMessage)).observe(doc.body); - observing = true; - } else { - console.error('observe called more than once'); - } -} diff --git a/src/worker-thread/Event.ts b/src/worker-thread/Event.ts index c62cf0a49..bf193adb9 100644 --- a/src/worker-thread/Event.ts +++ b/src/worker-thread/Event.ts @@ -16,6 +16,9 @@ import { Node } from './dom/Node'; import { TransferrableKeys } from '../transfer/TransferrableKeys'; +import { EventToWorker, MessageType } from '../transfer/Messages'; +import { TransferrableEvent } from '../transfer/TransferrableEvent'; +import { get } from './nodes'; interface EventOptions { bubbles?: boolean; @@ -57,3 +60,41 @@ export class Event { this.defaultPrevented = true; } } + +/** + * When an event is dispatched from the main thread, it needs to be propagated in the worker thread. + * Propagate adds an event listener to the worker global scope and uses the WorkerDOM Node.dispatchEvent + * method to dispatch the transfered event in the worker thread. + */ +export function propagate(): void { + if (typeof addEventListener !== 'function') { + return; + } + addEventListener('message', ({ data }: { data: EventToWorker }) => { + if (data[TransferrableKeys.type] !== MessageType.EVENT) { + return; + } + + const event = data[TransferrableKeys.event] as TransferrableEvent; + const node = get(event[TransferrableKeys.index]); + if (node !== null) { + const target = event[TransferrableKeys.target]; + node.dispatchEvent( + Object.assign( + new Event(event[TransferrableKeys.type], { bubbles: event[TransferrableKeys.bubbles], cancelable: event[TransferrableKeys.cancelable] }), + { + cancelBubble: event[TransferrableKeys.cancelBubble], + defaultPrevented: event[TransferrableKeys.defaultPrevented], + eventPhase: event[TransferrableKeys.eventPhase], + isTrusted: event[TransferrableKeys.isTrusted], + returnValue: event[TransferrableKeys.returnValue], + target: get(target ? target[0] : null), + timeStamp: event[TransferrableKeys.timeStamp], + scoped: event[TransferrableKeys.scoped], + keyCode: event[TransferrableKeys.keyCode], + }, + ), + ); + } + }); +} diff --git a/src/worker-thread/EventPropagation.ts b/src/worker-thread/EventPropagation.ts deleted file mode 100644 index 3e6bdc1c8..000000000 --- a/src/worker-thread/EventPropagation.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright 2018 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Event } from './Event'; -import { MessageType, EventToWorker } from '../transfer/Messages'; -import { TransferrableEvent } from '../transfer/TransferrableEvent'; -import { TransferrableKeys } from '../transfer/TransferrableKeys'; -import { get } from './nodes'; - -/** - * When an event is dispatched from the main thread, it needs to be propagated in the worker thread. - * Propagate adds an event listener to the worker global scope and uses the WorkerDOM Node.dispatchEvent - * method to dispatch the transfered event in the worker thread. - */ -export function propagate(): void { - if (typeof addEventListener !== 'function') { - return; - } - addEventListener('message', ({ data }: { data: EventToWorker }) => { - if (data[TransferrableKeys.type] !== MessageType.EVENT) { - return; - } - - const event = data[TransferrableKeys.event] as TransferrableEvent; - const node = get(event[TransferrableKeys.index]); - if (node !== null) { - const target = event[TransferrableKeys.target]; - node.dispatchEvent( - Object.assign( - new Event(event[TransferrableKeys.type], { bubbles: event[TransferrableKeys.bubbles], cancelable: event[TransferrableKeys.cancelable] }), - { - cancelBubble: event[TransferrableKeys.cancelBubble], - defaultPrevented: event[TransferrableKeys.defaultPrevented], - eventPhase: event[TransferrableKeys.eventPhase], - isTrusted: event[TransferrableKeys.isTrusted], - returnValue: event[TransferrableKeys.returnValue], - target: get(target ? target[TransferrableKeys.index] : null), - timeStamp: event[TransferrableKeys.timeStamp], - scoped: event[TransferrableKeys.scoped], - keyCode: event[TransferrableKeys.keyCode], - }, - ), - ); - } - }); -} diff --git a/src/worker-thread/MutationObserver.ts b/src/worker-thread/MutationObserver.ts index 338cab72e..876c5f2a1 100644 --- a/src/worker-thread/MutationObserver.ts +++ b/src/worker-thread/MutationObserver.ts @@ -17,6 +17,8 @@ import { Node } from './dom/Node'; import { MutationRecord } from './MutationRecord'; import { TransferrableKeys } from '../transfer/TransferrableKeys'; +import { Document } from './dom/Document'; +import { transfer } from './MutationTransfer'; const observers: MutationObserver[] = []; let pendingMutations = false; @@ -41,24 +43,17 @@ const pushMutation = (observer: MutationObserver, record: MutationRecord): void * These records are then pushed into MutationObserver instances that match the MutationRecord.target * @param record MutationRecord to push into MutationObservers. */ -export function mutate(record: MutationRecord): void { - observers.forEach(observer => { - if (!observer.options.flatten) { - // TODO: Restore? || record.type === MutationRecordType.COMMAND - pushMutation(observer, record); - return; - } +export function mutate(document: Document, record: MutationRecord, transferable: Array): void { + transfer(document.postMessage, transferable); + observers.forEach(observer => { let target: Node | null = record.target; - let matched = match(observer.target, target); - if (!matched) { - do { - if ((matched = match(observer.target, target))) { - pushMutation(observer, record); - break; - } - } while ((target = target.parentNode)); - } + do { + if (match(observer.target, target)) { + pushMutation(observer, record); + break; + } + } while ((target = target.parentNode)); }); } @@ -71,10 +66,6 @@ interface MutationObserverInit { // characterDataOldValue?: boolean; // childList?: boolean; // Default false // subtree?: boolean; // Default false - - // Except for this one (not specced) that will force all mutations to be observed - // Without flattening the record to the node requested to be observed. - flatten?: boolean; } export class MutationObserver { @@ -95,7 +86,7 @@ export class MutationObserver { public observe(target: Node, options?: MutationObserverInit): void { this.disconnect(); this.target = target; - this.options = Object.assign({ flatten: false }, options); + this.options = options || {}; observers.push(this); } diff --git a/src/worker-thread/MutationRecord.ts b/src/worker-thread/MutationRecord.ts index ef1ed2647..95247f52b 100644 --- a/src/worker-thread/MutationRecord.ts +++ b/src/worker-thread/MutationRecord.ts @@ -15,19 +15,6 @@ */ import { Node } from './dom/Node'; -import { TransferrableEventSubscriptionChange } from '../transfer/TransferrableEvent'; - -export type MutationRecordMutableKey = - | 'addedNodes' - | 'removedNodes' - | 'previousSibling' - | 'nextSibling' - | 'attributeName' - | 'attributeNamespace' - | 'propertyName' - | 'value' - | 'addedEvents' - | 'removedEvents'; // MutationRecord interface is modification and extension of the spec version. // It supports capturing property changes. @@ -44,13 +31,7 @@ export interface MutationRecord { // MutationRecord Extensions readonly type: MutationRecordType; - // Modifications of properties pass the property name modified. - readonly propertyName?: string | null; - // Mutation of attributes or properties must pass a value representing the new value. readonly value?: string | null; - // Event subscription mutations - readonly addedEvents?: Array; - readonly removedEvents?: Array; } // Add a new types of MutationRecord to capture changes not normally reported by MutationObserver on Nodes. @@ -60,9 +41,4 @@ export const enum MutationRecordType { ATTRIBUTES = 0, CHARACTER_DATA = 1, CHILD_LIST = 2, - PROPERTIES = 3, - EVENT_SUBSCRIPTION = 4, - GET_BOUNDING_CLIENT_RECT = 5, - LONG_TASK_START = 6, - LONG_TASK_END = 7, } diff --git a/src/worker-thread/MutationTransfer.ts b/src/worker-thread/MutationTransfer.ts new file mode 100644 index 000000000..f8bd4b83d --- /dev/null +++ b/src/worker-thread/MutationTransfer.ts @@ -0,0 +1,64 @@ +/** + * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { consume as consumeNodes } from './nodes'; +import { consume as consumeStrings } from './strings'; +import { MessageType } from '../transfer/Messages'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; +import { Node } from './dom/Node'; +import { Phase } from '../transfer/Phase'; +import { phase, set as setPhase } from './phase'; +import { PostMessage } from './worker-thread'; + +let allowTransfer = false; +let pending = false; +let pendingMutations: Array = []; + +export function transfer(postMessage: PostMessage, mutation: Array): void { + if (phase > Phase.Hydrating) { + pending = true; + pendingMutations = pendingMutations.concat(mutation); + + if (allowTransfer) { + Promise.resolve().then(_ => { + if (pending) { + const nodes = new Uint16Array( + consumeNodes().reduce((acc: Array, node: Node) => acc.concat(node[TransferrableKeys.creationFormat]), []), + ).buffer; + const mutations = new Uint16Array(pendingMutations).buffer; + + postMessage( + { + [TransferrableKeys.type]: MessageType.MUTATE, + [TransferrableKeys.nodes]: nodes, + [TransferrableKeys.strings]: consumeStrings(), + [TransferrableKeys.mutations]: mutations, + }, + [nodes, mutations], + ); + + pendingMutations = []; + pending = false; + } + }); + } + } +} + +export function observe(): void { + allowTransfer = true; + setPhase(Phase.Mutating); +} diff --git a/src/worker-thread/css/CSSStyleDeclaration.ts b/src/worker-thread/css/CSSStyleDeclaration.ts index 76257905c..f645b74bc 100644 --- a/src/worker-thread/css/CSSStyleDeclaration.ts +++ b/src/worker-thread/css/CSSStyleDeclaration.ts @@ -20,6 +20,9 @@ import { Element } from '../dom/Element'; import { NamespaceURI } from '../dom/Node'; import { toLower } from '../../utils'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { store as storeString } from '../strings'; +import { Document } from '../dom/Document'; interface StyleProperties { [key: string]: string | null; @@ -176,12 +179,22 @@ export class CSSStyleDeclaration implements StyleDeclaration { */ private mutated(value: string): void { const oldValue = this[TransferrableKeys.storeAttribute](this[TransferrableKeys.target].namespaceURI, 'style', value); - mutate({ - type: MutationRecordType.ATTRIBUTES, - target: this[TransferrableKeys.target], - attributeName: 'style', - value, - oldValue, - }); + mutate( + this[TransferrableKeys.target].ownerDocument as Document, + { + type: MutationRecordType.ATTRIBUTES, + target: this[TransferrableKeys.target], + attributeName: 'style', + value, + oldValue, + }, + [ + TransferrableMutationType.ATTRIBUTES, + this[TransferrableKeys.target][TransferrableKeys.index], + storeString('style'), + 0, // Attribute Namespace is the default value. + value !== null ? storeString(value) + 1 : 0, + ], + ); } } diff --git a/src/worker-thread/dom/CharacterData.ts b/src/worker-thread/dom/CharacterData.ts index 73edd7b87..328518fec 100644 --- a/src/worker-thread/dom/CharacterData.ts +++ b/src/worker-thread/dom/CharacterData.ts @@ -17,8 +17,11 @@ import { Node, NodeName } from './Node'; import { mutate } from '../MutationObserver'; import { MutationRecordType } from '../MutationRecord'; +import { store as storeString } from '../strings'; +import { Document } from './Document'; import { NodeType } from '../../transfer/TransferrableNodes'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; // @see https://developer.mozilla.org/en-US/docs/Web/API/CharacterData export abstract class CharacterData extends Node { @@ -52,12 +55,16 @@ export abstract class CharacterData extends Node { const oldValue = this.data; this[TransferrableKeys.data] = value; - mutate({ - target: this, - type: MutationRecordType.CHARACTER_DATA, - value, - oldValue, - }); + mutate( + this.ownerDocument as Document, + { + target: this, + type: MutationRecordType.CHARACTER_DATA, + value, + oldValue, + }, + [TransferrableMutationType.CHARACTER_DATA, this[TransferrableKeys.index], storeString(value)], + ); } /** diff --git a/src/worker-thread/dom/Comment.ts b/src/worker-thread/dom/Comment.ts index e0bf4c611..187e385a7 100644 --- a/src/worker-thread/dom/Comment.ts +++ b/src/worker-thread/dom/Comment.ts @@ -16,22 +16,24 @@ import { CharacterData } from './CharacterData'; import { NumericBoolean } from '../../utils'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { NodeType } from '../../transfer/TransferrableNodes'; import { store as storeString } from '../strings'; import { Node } from './Node'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { NodeType } from '../../transfer/TransferrableNodes'; // @see https://developer.mozilla.org/en-US/docs/Web/API/Comment export class Comment extends CharacterData { constructor(data: string, ownerDocument: Node) { super(data, NodeType.COMMENT_NODE, '#comment', ownerDocument); - this[TransferrableKeys.creationFormat] = { - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.transferred]: NumericBoolean.FALSE, - [TransferrableKeys.nodeType]: NodeType.COMMENT_NODE, - [TransferrableKeys.localOrNodeName]: storeString(this.nodeName), - [TransferrableKeys.textContent]: storeString(this.data), - }; + this[TransferrableKeys.creationFormat] = [ + this[TransferrableKeys.index], + NodeType.COMMENT_NODE, + storeString(this.nodeName), + NumericBoolean.TRUE, + storeString(this.data), + NumericBoolean.FALSE, + 0, + ]; } /** diff --git a/src/worker-thread/dom/DOMTokenList.ts b/src/worker-thread/dom/DOMTokenList.ts index dd217ae7a..40ecc4501 100644 --- a/src/worker-thread/dom/DOMTokenList.ts +++ b/src/worker-thread/dom/DOMTokenList.ts @@ -19,6 +19,9 @@ import { NamespaceURI } from './Node'; import { mutate } from '../MutationObserver'; import { MutationRecordType } from '../MutationRecord'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { store as storeString } from '../strings'; +import { Document } from './Document'; export class DOMTokenList { private [TransferrableKeys.tokens]: Array = []; @@ -188,12 +191,22 @@ export class DOMTokenList { */ private mutated(oldValue: string, value: string): void { this[TransferrableKeys.storeAttribute](this[TransferrableKeys.target].namespaceURI, this[TransferrableKeys.attributeName], value); - mutate({ - type: MutationRecordType.ATTRIBUTES, - target: this[TransferrableKeys.target], - attributeName: this[TransferrableKeys.attributeName], - value, - oldValue, - }); + mutate( + this[TransferrableKeys.target].ownerDocument as Document, + { + type: MutationRecordType.ATTRIBUTES, + target: this[TransferrableKeys.target], + attributeName: this[TransferrableKeys.attributeName], + value, + oldValue, + }, + [ + TransferrableMutationType.ATTRIBUTES, + this[TransferrableKeys.target][TransferrableKeys.index], + storeString(this[TransferrableKeys.attributeName]), + 0, // Attribute Namespace is the default value. + value !== null ? storeString(value) + 1 : 0, + ], + ); } } diff --git a/src/worker-thread/dom/Document.ts b/src/worker-thread/dom/Document.ts index e2f09a4ad..681adebd8 100644 --- a/src/worker-thread/dom/Document.ts +++ b/src/worker-thread/dom/Document.ts @@ -52,12 +52,13 @@ import { Event } from '../Event'; import { Text } from './Text'; import { Comment } from './Comment'; import { MutationObserver } from '../MutationObserver'; -import { NodeType, HTML_NAMESPACE } from '../../transfer/TransferrableNodes'; -import { observe as observeMutations } from '../DocumentMutations'; -import { propagate as propagateEvents } from '../EventPropagation'; -import { propagate as propagateSyncValues } from '../SyncValuePropagation'; import { toLower } from '../../utils'; import { DocumentFragment } from './DocumentFragment'; +import { PostMessage } from '../worker-thread'; +import { observe } from '../MutationTransfer'; +import { NodeType, HTML_NAMESPACE } from '../../transfer/TransferrableNodes'; +import { propagate as propagateEvents } from '../Event'; +import { propagate as propagateSyncValues } from '../SyncValuePropagation'; const DOCUMENT_NAME = '#document'; @@ -75,6 +76,7 @@ export class Document extends Element { }; public documentElement: Document; public body: Element; + public postMessage: PostMessage; constructor() { super(NodeType.DOCUMENT_NODE, DOCUMENT_NAME, HTML_NAMESPACE, null); @@ -82,7 +84,8 @@ export class Document extends Element { this.nodeName = DOCUMENT_NAME; this.documentElement = this; this.observe = (): void => { - observeMutations(this, this.postMessageMethod); + // Sync Document Changes. + observe(); propagateEvents(); propagateSyncValues(); }; @@ -128,11 +131,10 @@ export class Document extends Element { /** * - * @param postMessageMethod */ -export function createDocument(postMessageMethod?: Function): Document { +export function createDocument(postMessage?: PostMessage): Document { const doc = new Document(); - doc.postMessageMethod = postMessageMethod; + doc.postMessage = postMessage || (() => void 0); doc.isConnected = true; doc.appendChild((doc.body = doc.createElement('body'))); diff --git a/src/worker-thread/dom/DocumentFragment.ts b/src/worker-thread/dom/DocumentFragment.ts index 0ce449e0f..c7a83605e 100644 --- a/src/worker-thread/dom/DocumentFragment.ts +++ b/src/worker-thread/dom/DocumentFragment.ts @@ -15,22 +15,25 @@ */ import { ParentNode } from './ParentNode'; -import { NodeType } from '../../transfer/TransferrableNodes'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; import { NumericBoolean } from '../../utils'; import { store as storeString } from '../strings'; import { Node } from './Node'; +import { NodeType } from '../../transfer/TransferrableNodes'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; export class DocumentFragment extends ParentNode { constructor(ownerDocument: Node) { super(NodeType.DOCUMENT_FRAGMENT_NODE, '#document-fragment', ownerDocument); - this[TransferrableKeys.creationFormat] = { - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.transferred]: NumericBoolean.FALSE, - [TransferrableKeys.nodeType]: NodeType.DOCUMENT_FRAGMENT_NODE, - [TransferrableKeys.localOrNodeName]: storeString(this.nodeName), - }; + this[TransferrableKeys.creationFormat] = [ + this[TransferrableKeys.index], + NodeType.DOCUMENT_FRAGMENT_NODE, + storeString(this.nodeName), + NumericBoolean.FALSE, + 0, + NumericBoolean.FALSE, + 0, + ]; } /** diff --git a/src/worker-thread/dom/Element.ts b/src/worker-thread/dom/Element.ts index f10a2c9d0..3fd314da0 100644 --- a/src/worker-thread/dom/Element.ts +++ b/src/worker-thread/dom/Element.ts @@ -21,15 +21,17 @@ import { Attr, toString as attrsToString, matchPredicate as matchAttrPredicate } import { mutate } from '../MutationObserver'; import { MutationRecordType } from '../MutationRecord'; import { NumericBoolean, toLower, toUpper } from '../../utils'; -import { Text } from './Text'; import { CSSStyleDeclaration } from '../css/CSSStyleDeclaration'; import { matchChildrenElements } from './matchElements'; import { reflectProperties } from './enhanceElement'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { HydrateableNode, NodeType, HTML_NAMESPACE } from '../../transfer/TransferrableNodes'; import { store as storeString } from '../strings'; +import { Document } from './Document'; +import { transfer } from '../MutationTransfer'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { NodeType, HTML_NAMESPACE } from '../../transfer/TransferrableNodes'; +import { TransferrableBoundingClientRect } from '../../transfer/TransferrableBoundClientRect'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; import { MessageToWorker, MessageType, BoundingClientRectToWorker } from '../../transfer/Messages'; -import { TransferrableBoundingClientRect } from '../../transfer/TransferrableCommands'; import { parse } from '../../third_party/html-parser/html-parser'; import { propagate } from './Node'; @@ -91,28 +93,15 @@ export class Element extends ParentNode { this.namespaceURI = namespaceURI || HTML_NAMESPACE; this.localName = localName; this.kind = VOID_ELEMENTS.includes(this.tagName) ? ElementKind.VOID : ElementKind.NORMAL; - this[TransferrableKeys.creationFormat] = { - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.transferred]: NumericBoolean.FALSE, - [TransferrableKeys.nodeType]: this.nodeType, - [TransferrableKeys.localOrNodeName]: storeString(this.localName), - [TransferrableKeys.namespaceURI]: this.namespaceURI === null ? undefined : storeString(this.namespaceURI), - }; - } - - /** - * When hydrating the tree, we need to send HydrateableNode representations - * for the main thread to process and store items from for future modifications. - */ - public hydrate(): HydrateableNode { - return Object.assign(this[TransferrableKeys.creationFormat], { - [TransferrableKeys.childNodes]: this.childNodes.map(node => node.hydrate()), - [TransferrableKeys.attributes]: this.attributes.map(attribute => [ - storeString(attribute.namespaceURI || 'null'), - storeString(attribute.name), - storeString(attribute.value), - ]), - }); + this[TransferrableKeys.creationFormat] = [ + this[TransferrableKeys.index], + this.nodeType, + storeString(this.localName), + NumericBoolean.FALSE, + 0, + this.namespaceURI === null ? NumericBoolean.FALSE : NumericBoolean.TRUE, + this.namespaceURI === null ? 0 : storeString(this.namespaceURI), + ]; } // Unimplemented properties @@ -224,18 +213,28 @@ export class Element extends ParentNode { propagate(n, TransferrableKeys.scopingRoot, n); }); - mutate({ - removedNodes: this.childNodes, - type: MutationRecordType.CHILD_LIST, - target: this, - }); + mutate( + this.ownerDocument as Document, + { + removedNodes: this.childNodes, + type: MutationRecordType.CHILD_LIST, + target: this, + }, + [ + TransferrableMutationType.CHILD_LIST, + this[TransferrableKeys.index], + 0, + 0, + 0, + this.childNodes.length, + ...this.childNodes.map(node => node[TransferrableKeys.index]), + ], + ); this.childNodes = []; // add new children - root.childNodes.forEach((n: Node) => { - this.appendChild(n); - }); + root.childNodes.forEach((child: Node) => this.appendChild(child)); } /** @@ -244,8 +243,8 @@ export class Element extends ParentNode { */ set textContent(text: string) { // TODO(KB): Investigate removing all children in a single .splice to childNodes. - this.childNodes.forEach(childNode => childNode.remove()); - this.appendChild(new Text(text, this.ownerDocument)); + this.childNodes.slice().forEach((child: Node) => child.remove()); + this.appendChild(this.ownerDocument.createTextNode(text)); } /** @@ -338,14 +337,24 @@ export class Element extends ParentNode { } const oldValue = this[TransferrableKeys.storeAttribute](namespaceURI, name, valueAsString); - mutate({ - type: MutationRecordType.ATTRIBUTES, - target: this, - attributeName: name, - attributeNamespace: namespaceURI, - value: valueAsString, - oldValue, - }); + mutate( + this.ownerDocument as Document, + { + type: MutationRecordType.ATTRIBUTES, + target: this, + attributeName: name, + attributeNamespace: namespaceURI, + value: valueAsString, + oldValue, + }, + [ + TransferrableMutationType.ATTRIBUTES, + this[TransferrableKeys.index], + storeString(name), + storeString(namespaceURI), + value !== null ? storeString(valueAsString) + 1 : 0, + ], + ); } public [TransferrableKeys.storeAttribute](namespaceURI: NamespaceURI, name: string, value: string): string { @@ -397,13 +406,23 @@ export class Element extends ParentNode { const oldValue = this.attributes[index].value; this.attributes.splice(index, 1); - mutate({ - type: MutationRecordType.ATTRIBUTES, - target: this, - attributeName: name, - attributeNamespace: namespaceURI, - oldValue, - }); + mutate( + this.ownerDocument as Document, + { + type: MutationRecordType.ATTRIBUTES, + target: this, + attributeName: name, + attributeNamespace: namespaceURI, + oldValue, + }, + [ + TransferrableMutationType.ATTRIBUTES, + this[TransferrableKeys.index], + storeString(name), + storeString(namespaceURI), + 0, // 0 means no value + ], + ); } } @@ -479,37 +498,33 @@ export class Element extends ParentNode { }; return new Promise(resolve => { + const messageHandler = ({ data }: { data: MessageToWorker }) => { + if ( + data[TransferrableKeys.type] === MessageType.GET_BOUNDING_CLIENT_RECT && + (data as BoundingClientRectToWorker)[TransferrableKeys.target][0] === this[TransferrableKeys.index] + ) { + removeEventListener('message', messageHandler); + const transferredBoundingClientRect: TransferrableBoundingClientRect = (data as BoundingClientRectToWorker)[TransferrableKeys.data]; + resolve({ + top: transferredBoundingClientRect[0], + right: transferredBoundingClientRect[1], + bottom: transferredBoundingClientRect[2], + left: transferredBoundingClientRect[3], + width: transferredBoundingClientRect[4], + height: transferredBoundingClientRect[5], + x: transferredBoundingClientRect[0], + y: transferredBoundingClientRect[3], + }); + } + }; + if (typeof addEventListener !== 'function' || !this.isConnected) { // Elements run within Node runtimes are missing addEventListener as a global. // In this case, treat the return value the same as a disconnected node. resolve(defaultValue); } else { - addEventListener('message', ({ data }: { data: MessageToWorker }) => { - if ( - data[TransferrableKeys.type] === MessageType.GET_BOUNDING_CLIENT_RECT && - (data as BoundingClientRectToWorker)[TransferrableKeys.target][TransferrableKeys.index] === this[TransferrableKeys.index] - ) { - const transferredBoundingClientRect: TransferrableBoundingClientRect = (data as BoundingClientRectToWorker)[TransferrableKeys.data]; - resolve({ - top: transferredBoundingClientRect[0], - right: transferredBoundingClientRect[1], - bottom: transferredBoundingClientRect[2], - left: transferredBoundingClientRect[3], - width: transferredBoundingClientRect[4], - height: transferredBoundingClientRect[5], - x: transferredBoundingClientRect[0], - y: transferredBoundingClientRect[3], - }); - } - }); - // Requesting a boundingClientRect can be depdendent on mutations that have not yet - // applied in the main thread. As a result, ensure proper order of DOM mutation and reads - // by sending the request for a boundingClientRect as a mutation. - mutate({ - type: MutationRecordType.GET_BOUNDING_CLIENT_RECT, - target: this, - }); - + addEventListener('message', messageHandler); + transfer((this.ownerDocument as Document).postMessage, [TransferrableMutationType.GET_BOUNDING_CLIENT_RECT, this[TransferrableKeys.index]]); setTimeout(resolve, 500, defaultValue); // TODO: Why a magical constant, define and explain. } }); diff --git a/src/worker-thread/dom/HTMLInputElement.ts b/src/worker-thread/dom/HTMLInputElement.ts index 34b208715..f060346ce 100644 --- a/src/worker-thread/dom/HTMLInputElement.ts +++ b/src/worker-thread/dom/HTMLInputElement.ts @@ -14,13 +14,15 @@ * limitations under the License. */ -import { mutate } from '../MutationObserver'; import { HTMLElement } from './HTMLElement'; import { HTMLInputLabelsMixin } from './HTMLInputLabelsMixin'; -import { MutationRecordType } from '../MutationRecord'; import { reflectProperties } from './enhanceElement'; import { registerSubclass } from './Element'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { store as storeString } from '../strings'; +import { Document } from './Document'; +import { transfer } from '../MutationTransfer'; export class HTMLInputElement extends HTMLElement { // Per spec, some attributes like 'value' and 'checked' change behavior based on dirty flags. @@ -45,12 +47,12 @@ export class HTMLInputElement extends HTMLElement { // Don't early-out if value doesn't appear to have changed. // The worker may have a stale value since 'input' events aren't being forwarded. this[TransferrableKeys.value] = String(value); - mutate({ - type: MutationRecordType.PROPERTIES, - target: this, - propertyName: 'value', - value, - }); + transfer((this.ownerDocument as Document).postMessage, [ + TransferrableMutationType.PROPERTIES, + this[TransferrableKeys.index], + storeString('value'), + storeString(value), + ]); } get valueAsDate(): Date | null { @@ -93,13 +95,12 @@ export class HTMLInputElement extends HTMLElement { return; } this[TransferrableKeys.checked] = !!value; - mutate({ - type: MutationRecordType.PROPERTIES, - target: this, - propertyName: 'checked', - // TODO(choumx, #122): Proper support for non-string property mutations. - value: String(value), - }); + transfer((this.ownerDocument as Document).postMessage, [ + TransferrableMutationType.PROPERTIES, + this[TransferrableKeys.index], + storeString('checked'), + storeString(String(value)), + ]); } /** diff --git a/src/worker-thread/dom/Node.ts b/src/worker-thread/dom/Node.ts index 781c6c910..6931df244 100644 --- a/src/worker-thread/dom/Node.ts +++ b/src/worker-thread/dom/Node.ts @@ -16,12 +16,15 @@ import { store as storeNodeMapping } from '../nodes'; import { Event, EventHandler } from '../Event'; -import { toLower, NumericBoolean } from '../../utils'; +import { toLower } from '../../utils'; import { mutate } from '../MutationObserver'; import { MutationRecordType } from '../MutationRecord'; -import { TransferredNode, TransferrableNode, HydrateableNode, NodeType } from '../../transfer/TransferrableNodes'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; import { store as storeString } from '../strings'; +import { Document } from './Document'; +import { transfer } from '../MutationTransfer'; +import { TransferredNode, NodeType } from '../../transfer/TransferrableNodes'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; export type NodeName = '#comment' | '#document' | '#document-fragment' | '#text' | string; export type NamespaceURI = string; @@ -55,7 +58,7 @@ export abstract class Node { public isConnected: boolean = false; public [TransferrableKeys.index]: number; public [TransferrableKeys.transferredFormat]: TransferredNode; - public [TransferrableKeys.creationFormat]: TransferrableNode; + public [TransferrableKeys.creationFormat]: Array; public abstract cloneNode(deep: boolean): Node; private [TransferrableKeys.handlers]: { [index: string]: EventHandler[]; @@ -69,18 +72,7 @@ export abstract class Node { this[TransferrableKeys.scopingRoot] = this; this[TransferrableKeys.index] = storeNodeMapping(this); - this[TransferrableKeys.transferredFormat] = { - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.transferred]: NumericBoolean.TRUE, - }; - } - - /** - * When hydrating the tree, we need to send HydrateableNode representations - * for the main thread to process and store items from for future modifications. - */ - public hydrate(): HydrateableNode { - return this[TransferrableKeys.creationFormat]; + this[TransferrableKeys.transferredFormat] = [this[TransferrableKeys.index]]; } // Unimplemented Properties @@ -218,12 +210,37 @@ export abstract class Node { child.parentNode = this; propagate(child, 'isConnected', this.isConnected); propagate(child, TransferrableKeys.scopingRoot, this[TransferrableKeys.scopingRoot]); - mutate({ - addedNodes: [child], - nextSibling: referenceNode, - type: MutationRecordType.CHILD_LIST, - target: this, - }); + mutate( + this.ownerDocument as Document, + { + addedNodes: [child], + nextSibling: referenceNode, + type: MutationRecordType.CHILD_LIST, + target: this, + }, + [ + TransferrableMutationType.CHILD_LIST, + this[TransferrableKeys.index], + referenceNode[TransferrableKeys.index], + 0, + 1, + 0, + child[TransferrableKeys.index], + ], + ); + + /* + [ + TransferrableMutationType.CHILD_LIST, + Target.index, + NextSibling.index, + PreviousSibling.index, + AppendedNodeCount, + RemovedNodeCount, + ... AppendedNode.index, + ... RemovedNode.index, + ] + */ return child; } @@ -247,12 +264,25 @@ export abstract class Node { propagate(child, TransferrableKeys.scopingRoot, this[TransferrableKeys.scopingRoot]); this.childNodes.push(child); - mutate({ - addedNodes: [child], - previousSibling: this.childNodes[this.childNodes.length - 2], - type: MutationRecordType.CHILD_LIST, - target: this, - }); + const previousSibling = this.childNodes[this.childNodes.length - 2]; + mutate( + this.ownerDocument as Document, + { + addedNodes: [child], + previousSibling, + type: MutationRecordType.CHILD_LIST, + target: this, + }, + [ + TransferrableMutationType.CHILD_LIST, + this[TransferrableKeys.index], + 0, + previousSibling ? previousSibling[TransferrableKeys.index] : 0, + 1, + 0, + child[TransferrableKeys.index], + ], + ); } return child; } @@ -272,11 +302,15 @@ export abstract class Node { propagate(child, 'isConnected', false); propagate(child, TransferrableKeys.scopingRoot, child); this.childNodes.splice(index, 1); - mutate({ - removedNodes: [child], - type: MutationRecordType.CHILD_LIST, - target: this, - }); + mutate( + this.ownerDocument as Document, + { + removedNodes: [child], + type: MutationRecordType.CHILD_LIST, + target: this, + }, + [TransferrableMutationType.CHILD_LIST, this[TransferrableKeys.index], 0, 0, 0, 1, child[TransferrableKeys.index]], + ); return child; } @@ -317,13 +351,27 @@ export abstract class Node { propagate(newChild, 'isConnected', this.isConnected); propagate(newChild, TransferrableKeys.scopingRoot, this[TransferrableKeys.scopingRoot]); - mutate({ - addedNodes: [newChild], - removedNodes: [oldChild], - type: MutationRecordType.CHILD_LIST, - nextSibling: this.childNodes[index + 1], - target: this, - }); + mutate( + this.ownerDocument as Document, + { + addedNodes: [newChild], + removedNodes: [oldChild], + type: MutationRecordType.CHILD_LIST, + nextSibling: this.childNodes[index + 1], + target: this, + }, + [ + TransferrableMutationType.CHILD_LIST, + this[TransferrableKeys.index], + this.childNodes[index + 1] ? this.childNodes[index + 1][TransferrableKeys.index] : 0, + 0, + 1, + 1, + newChild[TransferrableKeys.index], + oldChild[TransferrableKeys.index], + ], + ); + return oldChild; } @@ -342,25 +390,24 @@ export abstract class Node { * @param handler Function called when event is dispatched. */ public addEventListener(type: string, handler: EventHandler): void { - const handlers: EventHandler[] = this[TransferrableKeys.handlers][toLower(type)]; + const lowerType = toLower(type); + const storedType = storeString(lowerType); + const handlers: EventHandler[] = this[TransferrableKeys.handlers][lowerType]; let index: number = 0; if (handlers) { index = handlers.push(handler); } else { - this[TransferrableKeys.handlers][toLower(type)] = [handler]; + this[TransferrableKeys.handlers][lowerType] = [handler]; } - mutate({ - target: this, - type: MutationRecordType.EVENT_SUBSCRIPTION, - addedEvents: [ - { - [TransferrableKeys.type]: storeString(type), - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.index]: index, - }, - ], - }); + transfer((this.ownerDocument as Document).postMessage, [ + TransferrableMutationType.EVENT_SUBSCRIPTION, + this[TransferrableKeys.index], + 0, + 1, + storedType, + index, + ]); } /** @@ -370,22 +417,20 @@ export abstract class Node { * @param handler Function to stop calling when event is dispatched. */ public removeEventListener(type: string, handler: EventHandler): void { - const handlers = this[TransferrableKeys.handlers][toLower(type)]; + const lowerType = toLower(type); + const handlers = this[TransferrableKeys.handlers][lowerType]; const index = !!handlers ? handlers.indexOf(handler) : -1; if (index >= 0) { handlers.splice(index, 1); - mutate({ - target: this, - type: MutationRecordType.EVENT_SUBSCRIPTION, - removedEvents: [ - { - [TransferrableKeys.type]: storeString(type), - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.index]: index, - }, - ], - }); + transfer((this.ownerDocument as Document).postMessage, [ + TransferrableMutationType.EVENT_SUBSCRIPTION, + this[TransferrableKeys.index], + 1, + 0, + storeString(lowerType), + index, + ]); } } diff --git a/src/worker-thread/dom/SVGElement.ts b/src/worker-thread/dom/SVGElement.ts index c01ae342f..88932d5e8 100644 --- a/src/worker-thread/dom/SVGElement.ts +++ b/src/worker-thread/dom/SVGElement.ts @@ -15,8 +15,8 @@ */ import { Element, registerSubclass } from './Element'; -import { SVG_NAMESPACE, NodeType } from '../../transfer/TransferrableNodes'; import { NodeName, Node, NamespaceURI } from './Node'; +import { SVG_NAMESPACE, NodeType } from '../../transfer/TransferrableNodes'; export class SVGElement extends Element { constructor(nodeType: NodeType, localName: NodeName, namespaceURI: NamespaceURI, ownerDocument: Node) { diff --git a/src/worker-thread/dom/Text.ts b/src/worker-thread/dom/Text.ts index 8cc54d2ef..923fcb0b2 100644 --- a/src/worker-thread/dom/Text.ts +++ b/src/worker-thread/dom/Text.ts @@ -16,22 +16,24 @@ import { CharacterData } from './CharacterData'; import { NumericBoolean } from '../../utils'; -import { TransferrableKeys } from '../../transfer/TransferrableKeys'; -import { NodeType } from '../../transfer/TransferrableNodes'; import { store as storeString } from '../strings'; import { Node } from './Node'; +import { NodeType } from '../../transfer/TransferrableNodes'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; // @see https://developer.mozilla.org/en-US/docs/Web/API/Text export class Text extends CharacterData { constructor(data: string, ownerDocument: Node) { super(data, NodeType.TEXT_NODE, '#text', ownerDocument); - this[TransferrableKeys.creationFormat] = { - [TransferrableKeys.index]: this[TransferrableKeys.index], - [TransferrableKeys.transferred]: NumericBoolean.FALSE, - [TransferrableKeys.nodeType]: NodeType.TEXT_NODE, - [TransferrableKeys.localOrNodeName]: storeString('#text'), - [TransferrableKeys.textContent]: storeString(this.data), - }; + this[TransferrableKeys.creationFormat] = [ + this[TransferrableKeys.index], + NodeType.TEXT_NODE, + storeString('#text'), + NumericBoolean.TRUE, + storeString(this.data), + NumericBoolean.FALSE, + 0, + ]; } // Unimplemented Properties diff --git a/src/worker-thread/dom/matchElements.ts b/src/worker-thread/dom/matchElements.ts index e787e184a..47a54327a 100644 --- a/src/worker-thread/dom/matchElements.ts +++ b/src/worker-thread/dom/matchElements.ts @@ -16,8 +16,8 @@ import { Element } from './Element'; import { toLower, toUpper } from '../../utils'; -import { NodeType } from '../../transfer/TransferrableNodes'; import { Node } from './Node'; +import { NodeType } from '../../transfer/TransferrableNodes'; export type ConditionPredicate = (element: Element) => boolean; // To future authors: It would be great if we could enforce that elements are not modified by a ConditionPredicate. diff --git a/src/worker-thread/index.safe.ts b/src/worker-thread/index.safe.ts index 040250161..944e30bd9 100644 --- a/src/worker-thread/index.safe.ts +++ b/src/worker-thread/index.safe.ts @@ -127,7 +127,7 @@ const WHITELISTED_GLOBALS = [ 'unescape', ]; -const doc = createDocument((self as DedicatedWorkerGlobalScope).postMessage); +const doc = createDocument(postMessage.bind(self)); export const workerDOM: WorkerDOMGlobalScope = { document: doc, navigator: (self as WorkerGlobalScope).navigator, diff --git a/src/worker-thread/index.ts b/src/worker-thread/index.ts index a2a2e915c..2dda4114f 100644 --- a/src/worker-thread/index.ts +++ b/src/worker-thread/index.ts @@ -47,9 +47,7 @@ import { WorkerDOMGlobalScope } from './WorkerDOMGlobalScope'; import { appendKeys } from './css/CSSStyleDeclaration'; import { consumeInitialDOM } from './initialize'; -declare var __ALLOW_POST_MESSAGE__: boolean; - -const doc = createDocument(__ALLOW_POST_MESSAGE__ ? (self as DedicatedWorkerGlobalScope).postMessage : undefined); +const doc = createDocument(postMessage.bind(self)); export const workerDOM: WorkerDOMGlobalScope = { document: doc, navigator: (self as WorkerGlobalScope).navigator, diff --git a/src/worker-thread/initialize.ts b/src/worker-thread/initialize.ts index 0b086d1c4..cb236b8b4 100644 --- a/src/worker-thread/initialize.ts +++ b/src/worker-thread/initialize.ts @@ -14,21 +14,21 @@ * limitations under the License. */ -import { HydrateableNode, NodeType } from '../transfer/TransferrableNodes'; import { store as storeString } from './strings'; import { store as storeNode } from './nodes'; -import { TransferrableKeys } from '../transfer/TransferrableKeys'; import { RenderableElement } from './worker-thread'; import { Document } from './dom/Document'; import { HTMLElement } from './dom/HTMLElement'; import { SVGElement } from './dom/SVGElement'; -import { Phase } from '../transfer/phase'; +import { HydrateableNode, NodeType } from '../transfer/TransferrableNodes'; +import { Phase } from '../transfer/Phase'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; import { set as setPhase } from './phase'; export function consumeInitialDOM(document: Document, strings: Array, hydrateableNode: HydrateableNode): void { + setPhase(Phase.Hydrating); strings.forEach(storeString); (hydrateableNode[TransferrableKeys.childNodes] || []).forEach(child => document.body.appendChild(create(document, strings, child))); - setPhase(Phase.Hydrating); } function create(document: Document, strings: Array, skeleton: HydrateableNode): RenderableElement { diff --git a/src/worker-thread/long-task.ts b/src/worker-thread/long-task.ts index 787cefa00..2ee8c6bb7 100644 --- a/src/worker-thread/long-task.ts +++ b/src/worker-thread/long-task.ts @@ -15,8 +15,10 @@ */ import { Node } from './dom/Node'; -import { MutationRecordType } from './MutationRecord'; -import { mutate } from './MutationObserver'; +import { transfer } from './MutationTransfer'; +import { Document } from './dom/Document'; +import { TransferrableMutationType } from '../transfer/TransferrableMutation'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; export function wrap(target: Node, func: Function): Function { return function() { @@ -24,18 +26,18 @@ export function wrap(target: Node, func: Function): Function { }; } -function execute(target: Node, promise: Promise, message?: string): Promise { +function execute(target: Node, promise: Promise): Promise { // Start the task. - mutate({ type: MutationRecordType.LONG_TASK_START, target }); + transfer((target.ownerDocument as Document).postMessage, [TransferrableMutationType.LONG_TASK_START, target[TransferrableKeys.index]]); return promise.then( result => { // Complete the task. - mutate({ type: MutationRecordType.LONG_TASK_END, target }); + transfer((target.ownerDocument as Document).postMessage, [TransferrableMutationType.LONG_TASK_END, target[TransferrableKeys.index]]); return result; }, reason => { // Complete the task. - mutate({ type: MutationRecordType.LONG_TASK_END, target }); + transfer((target.ownerDocument as Document).postMessage, [TransferrableMutationType.LONG_TASK_END, target[TransferrableKeys.index]]); throw reason; }, ); diff --git a/src/worker-thread/nodes.ts b/src/worker-thread/nodes.ts index 5d23baf1d..514c87b49 100644 --- a/src/worker-thread/nodes.ts +++ b/src/worker-thread/nodes.ts @@ -15,9 +15,9 @@ */ import { Node } from './dom/Node'; -import { Phase } from '../transfer/phase'; -import { TransferrableKeys } from '../transfer/TransferrableKeys'; import { phase } from './phase'; +import { Phase } from '../transfer/Phase'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; let count: number = 0; let transfer: Array = []; @@ -34,7 +34,7 @@ export function store(node: Node): number { } mapping.set((node[TransferrableKeys.index] = ++count), node); - if (phase !== Phase.Initializing) { + if (phase > Phase.Hydrating) { // After Initialization, include all future dom node creation into the list for next transfer. transfer.push(node); } diff --git a/src/worker-thread/phase.ts b/src/worker-thread/phase.ts index 4eae4a7fe..1d1fcbef4 100644 --- a/src/worker-thread/phase.ts +++ b/src/worker-thread/phase.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Phase } from '../transfer/phase'; +import { Phase } from '../transfer/Phase'; export let phase: Phase = Phase.Initializing; export const set = (newPhase: Phase) => (phase = newPhase); diff --git a/src/worker-thread/worker-thread.d.ts b/src/worker-thread/worker-thread.d.ts index 1868855a4..7e1d9df3c 100644 --- a/src/worker-thread/worker-thread.d.ts +++ b/src/worker-thread/worker-thread.d.ts @@ -20,3 +20,5 @@ import { Text } from './dom/Text'; import { Comment } from './dom/Comment'; type RenderableElement = HTMLElement | SVGElement | Text | Comment; +type PostMessage = (message: any, transfer?: Transferable[]) => void; +declare const DEBUG_ENABLED: boolean;