-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support callFunction API #850
Changes from 15 commits
e37e8bf
21e3da2
72cd523
3a6e20f
2547c67
520d143
ec3c674
07ce232
6b4af3f
b1a0596
f95383f
4d07722
926f295
11d7a50
d5405ad
7070959
382523d
078a66e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
function performComplexMath() { | ||
return Math.random() * 1000; | ||
} | ||
|
||
function getRemoteData() { | ||
return Promise.resolve({ big: 'tuna' }); | ||
} | ||
|
||
function immediatelyThrow() { | ||
throw new Error('Immediately threw'); | ||
} | ||
|
||
function reject() { | ||
return Promise.reject('Unsupported operation.'); | ||
} | ||
|
||
function returnsUndefined() { | ||
return Promise.resolve(undefined); | ||
} | ||
|
||
function add(n1, n2) { | ||
return n1 + n2; | ||
} | ||
|
||
function concat() { | ||
let combined = []; | ||
for (let i = 0; i < arguments.length; i++) { | ||
combined = combined.concat(arguments[i]); | ||
} | ||
return combined; | ||
} | ||
|
||
[performComplexMath, getRemoteData, immediatelyThrow, reject, add, concat, returnsUndefined].map((fn) => { | ||
exportFunction(fn.name, fn); | ||
}); | ||
|
||
// Manual test for .onerror, by scheduling an unhandled error | ||
// 2s in via prompt which isn't valid in a Worker. | ||
setTimeout(() => prompt(), 2000); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<title>Call function</title> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<script src="/dist/amp/main.mjs" type="module"></script> | ||
</head> | ||
<body> | ||
<div src="call-function.js" id="upgrade-me"></div> | ||
<script type="module"> | ||
import { upgradeElement } from "/dist/amp/main.mjs"; | ||
upgradeElement( | ||
document.getElementById("upgrade-me"), | ||
"/dist/amp/worker/worker.mjs" | ||
).then((worker) => { | ||
worker | ||
.callFunction("performComplexMath") | ||
.then((result) => console.log(`Complex math result: ${result}`)); | ||
worker | ||
.callFunction("getRemoteData") | ||
.then((result) => console.log(`Remote data: ${result}`)); | ||
worker | ||
.callFunction("add", 40, 2) | ||
.then((result) => console.log(`Answer to it all: ${result}`)); | ||
|
||
worker | ||
.callFunction("concat", [1, 2, 3], ["4", "5"]) | ||
.then((result) => | ||
console.log(`concat([1,2,3], ["4", "5"]) is: ${result}`) | ||
); | ||
|
||
worker.callFunction('returnsUndefined').then(result => { | ||
console.log(`undefined --> ${result}`) | ||
}) | ||
|
||
worker.callFunction("reject").catch((err) => console.error(err)); | ||
worker | ||
.callFunction("tooCoolToExist") | ||
.catch((err) => console.error(err)); | ||
worker | ||
.callFunction("immediatelyThrow") | ||
.catch((err) => console.error(err)); | ||
|
||
worker.onerror = (err) => { | ||
console.error("Catching an unhandled error: ", err); | ||
}; | ||
}); | ||
</script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"name": "@ampproject/worker-dom", | ||
"version": "0.24.0", | ||
"version": "0.25.0", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to manually update the version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how can I automatically update the version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The release script will do this automatically (check out RELEASING.md). |
||
"description": "A facsimile of a modern DOM implementation intended to run in a Web Worker.", | ||
"main": "dist/main", | ||
"files": [ | ||
|
@@ -95,7 +95,7 @@ | |
"brotli": "12.6 kB" | ||
}, | ||
"./dist/main.mjs": { | ||
"brotli": "3.5 kB" | ||
"brotli": "3.68 kB" | ||
}, | ||
"./dist/main.js": { | ||
"brotli": "4.1 kB" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* Copyright 2020 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 { CommandExecutorInterface } from './interface'; | ||
import { TransferrableMutationType, FunctionMutationIndex } from '../../transfer/TransferrableMutation'; | ||
import { ResolveOrReject } from '../../transfer/Messages'; | ||
|
||
let fnCallCount = 0; | ||
|
||
/** | ||
* A mapping between each request to callFunction and its Promise. | ||
*/ | ||
const promiseMap: { | ||
[id: number]: { | ||
promise: Promise<any>; | ||
resolve: (arg: any) => void; | ||
reject: (arg: any) => void; | ||
}; | ||
} = {}; | ||
|
||
/** | ||
* Each invocation of `ExportedWorker.prototype.callFunction` needs to be registered with a unique index | ||
* and promise. The index is given to the underlying Worker and returned by it as well. That enables the main-thread to | ||
* correlate postMessage responses with their original requests and resolve/reject the correct Promise. | ||
*/ | ||
export function registerPromise(): { promise: Promise<any>; index: number } { | ||
// TS won't realize that the constructor promise assigns the handlers, so we `any` them. | ||
let resolve: any; | ||
let reject: any; | ||
const promise = new Promise((res, rej) => { | ||
resolve = res; | ||
reject = rej; | ||
}); | ||
|
||
// Wraparound to 0 in case someone attempts to register over 9 quadrillion promises. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOL. This is likely not necessary, but funny nonetheless. |
||
if (fnCallCount >= Number.MAX_VALUE) { | ||
fnCallCount = 0; | ||
} | ||
const index = fnCallCount++; | ||
|
||
promiseMap[index] = { promise, resolve, reject }; | ||
return { promise, index }; | ||
} | ||
|
||
export const FunctionProcessor: CommandExecutorInterface = (strings, nodeContext, workerContext, objectContext, config) => { | ||
const allowedExecution = config.executorsAllowed.includes(TransferrableMutationType.FUNCTION_CALL); | ||
|
||
return { | ||
execute(mutations: Uint16Array, startPosition: number): number { | ||
if (allowedExecution) { | ||
const status = mutations[startPosition + FunctionMutationIndex.Status]; | ||
const index = mutations[startPosition + FunctionMutationIndex.Index]; | ||
const value = mutations[startPosition + FunctionMutationIndex.Value]; | ||
|
||
const parsed = strings.hasIndex(value) ? JSON.parse(strings.get(value)) : undefined; | ||
if (status === ResolveOrReject.RESOLVE) { | ||
promiseMap[index].resolve(parsed); | ||
} else { | ||
promiseMap[index].reject(parsed); | ||
} | ||
delete promiseMap[index]; | ||
} | ||
return startPosition + FunctionMutationIndex.End; | ||
}, | ||
|
||
print(mutations: Uint16Array, startPosition: number): {} { | ||
const status = mutations[startPosition + FunctionMutationIndex.Status]; | ||
const index = mutations[startPosition + FunctionMutationIndex.Index]; | ||
const value = mutations[startPosition + FunctionMutationIndex.Value]; | ||
|
||
return { | ||
type: 'FUNCTION_INVOCATION', | ||
status, | ||
index, | ||
value: strings.get(value), | ||
allowedExecution, | ||
}; | ||
}, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/** | ||
* Copyright 2020 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 { WorkerContext } from './worker'; | ||
import { WorkerDOMConfiguration } from './configuration'; | ||
import { registerPromise } from './commands/function'; | ||
import { FunctionCallToWorker, MessageType } from '../transfer/Messages'; | ||
import { TransferrableKeys } from '../transfer/TransferrableKeys'; | ||
import { TransferrableMutationType } from '../transfer/TransferrableMutation'; | ||
|
||
/** | ||
* An ExportedWorker is returned by the upgradeElement API. | ||
* For the most part, it delegates to the underlying Worker. | ||
* | ||
* It notably removes `postMessage` support and add `callFunction`. | ||
*/ | ||
export class ExportedWorker { | ||
samouri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
workerContext_: WorkerContext; | ||
config: WorkerDOMConfiguration; | ||
|
||
constructor(workerContext: WorkerContext, config: WorkerDOMConfiguration) { | ||
this.workerContext_ = workerContext; | ||
this.config = config; | ||
} | ||
|
||
/** | ||
* Calls a function in the worker and returns a promise with the result. | ||
* @param functionIdentifer | ||
* @param functionArguments | ||
*/ | ||
callFunction(functionIdentifer: string, ...functionArguments: any[]): Promise<any> { | ||
if (!this.config.executorsAllowed.includes(TransferrableMutationType.FUNCTION_CALL)) { | ||
throw new Error(`[worker-dom]: Error calling ${functionIdentifer}. You must enable the FUNCTION_CALL executor within the config.`); | ||
} | ||
|
||
const { promise, index } = registerPromise(); | ||
const msg: FunctionCallToWorker = { | ||
[TransferrableKeys.type]: MessageType.FUNCTION, | ||
[TransferrableKeys.functionIdentifier]: functionIdentifer, | ||
[TransferrableKeys.functionArguments]: JSON.stringify(functionArguments), | ||
[TransferrableKeys.index]: index, | ||
}; | ||
this.workerContext_.messageToWorker(msg); | ||
return promise; | ||
} | ||
|
||
set onerror(handler: any) { | ||
this.workerContext_.worker.onerror = handler; | ||
} | ||
|
||
terminate(): void { | ||
this.workerContext_.worker.terminate(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an await example to this demo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure thing!