Skip to content
This repository was archived by the owner on Dec 5, 2022. It is now read-only.

Extract tag handling from request handling #173

Merged
merged 23 commits into from
Sep 6, 2017

Conversation

DeTeam
Copy link
Contributor

@DeTeam DeTeam commented Aug 25, 2017

Motivation

Right now in tailor request-handler.js contains quite a lot of logic. It's responsible for several things:

  • Kicking of context and template fetching
  • Assembling of the tag-handling logic
  • Deciding on how to handle various events in the app (both in the result stream and in the primary fragment)
  • Takes care of async stream management

With so many responsibilities it's getting harder to allow more flexible composition to users of tailor. One particular use-case: we want to introduce a new tag, which itself would result into some template which has to be processed in the similar way. handleTag function already allows returning streams, but it's quite an overhead to duplicate existing handling algorithm.

Solution: extract tag/template handling logic and make it composable. Right now it's extracted under buildBlock which can be used inside your own implementation of handleTag, so we allow recursion.

Things which were standing in the way during implementation:

  • index management and asyncStream
  • shouldWriteHead was kind of everywhere
  • request object is all over the place + kinda need to separate tailor context (the one overriding fragment info) and more broad term context (some state which is being modified during template processing, for instance, index)

TODOs:

  • Separate tag-handling logic (working name - "block", though I don't really like it, mb we can come up with smth better)
  • Add support for per-fragments contexts (this is not directly related, so we might consider make a separate PR with it)
  • Make sure parsing template within buidBlock works properly
  • Extract and revert example changes
  • Replace package lock with yarn lock
  • Complete the unit tests
  • Check performance impact
  • Adjust the documentation

@DeTeam
Copy link
Contributor Author

DeTeam commented Aug 25, 2017

👎

@@ -0,0 +1,4 @@
<script type="text/javascript"></script>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without it, the pipe script would be inserted after the switcher and won't be available to it's content. @vigneshshanmugam mb you know the nice way to do it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah pipe is inserted before fragment tag or one of the tags in handleTags list. Just pass them in the example :)

Copy link
Contributor Author

@DeTeam DeTeam Aug 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean pipeTags? (it's internal)

Nevermind :)

@@ -0,0 +1,2754 @@
{
"name": "node-tailor",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use yarn.lock, can you update yarn.lock instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were no updates, I just happened to use latset npm & node %)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

index.js Outdated
@@ -64,7 +64,7 @@ module.exports = class Tailor extends EventEmitter {

requestOptions.parseTemplate = parseTemplate(
[requestOptions.fragmentTag].concat(requestOptions.handledTags),
['script', requestOptions.fragmentTag]
['script', requestOptions.fragmentTag].concat(requestOptions.handledTags)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vigneshshanmugam I guess you were meant this. Those custom handledTags where not passed to pipe tags, so for my switcher example it was not added. Not sure it's the best way, though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you are right. Just feels like this gives too many options of having where pipe should be inserted. Actually, pipe is never required if its not a fragment, "script" was added because anyone can add fragment in the head like <script src="blah" type="fragment"/>.

I would move the fragment in your example before switcher to make things easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or have a script tag :)
It was an interesting side-effect I didn't think about.
People who implement their own tags should kinda be aware then :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah true, lets remove these options and insert a empty div tag like before.


// Fragment server - Any http server that can serve fragments
http.createServer((req, res) => {
const urlObj = require('url').parse(req.url, true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move the require to the top.


Promise.all([
templatePromise,
contextPromise
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an error in context should not control the page and return 500.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a catch above.. Not super visible, though %)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool. missed it :)

lib/block.js Outdated

const resultStream = new StringifierStream((tag) => {

const {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this destructuring logic happens on every invocation of tag, should be moved out of of this function

lib/block.js Outdated
}
};

fetchDynamicContext(tag)
Copy link
Collaborator

@vigneshshanmugam vigneshshanmugam Aug 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this creates unnecessary complexity in tailor itself and can be handled outside in requestFragment logic.

Tailor itself is extendable, we may not need this feature here.

Thoughts? @grassator @mo-gr

Copy link
Contributor Author

@DeTeam DeTeam Aug 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially agreed :) It doesn't really belong to this PR, moving out.


fragment.on('fallback', (err) => {
this.emit('error', request, err);
// TODO: add some responseHeaders?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not responseHeaders like before? Do you have anything specific in mind?

Its also configurable through filterResponseHeaders function


fragment.on('error', (err) => {
this.emit('error', request, err);
// TODO: add some responseHeaders?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

}, options, context));


FRAGMENT_EVENTS.forEach((eventName) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming these events are for fragments. Could you explain me why it is required here?

Copy link
Contributor Author

@DeTeam DeTeam Aug 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of the tailor API (fragment:start, fragment:end, etc. events.) for fragments. It was there before (almost exact same code), difference is that now instead of subscribing to the fragment directly we subscribe to the "block" events (it propagates them), cause fragments are not directly exposed to the request handler.

Does this make sense to you?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah cool, looks a bit bad that we need to handle it twice.

@@ -9,6 +9,8 @@ describe('Serializer', () => {
const serializerOptions = {
treeAdapter: adapter,
slotMap: new Map(),
addPlaceholders: true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think its used anywhere.

lib/block.js Outdated
return pipeDefinition(pipeInstanceName);
}

if (asyncStream && placeholder === 'async') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think we need this asyncstream check. Its gonna be present all the time.

As an enhancement we can do this

  • create asyncstream and placeholders only when async fragments are present in the template.

lib/block.js Outdated
handleTag,
requestFragment,

// Should the two below be one???
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain what you mean here? having a single option instead of 3 separate options?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a note to myself: we have many pipe-related options, I was wondering if some of the options can be joined (the function part with the attribute part, cause we just pass one to another :D )

lib/block.js Outdated
});
});

const isAsync = fragment.attributes.async;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we keep the code similar to how it was in request-handler?

const { attributes: { async, primary }, stream } = fragment;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed!

Wow, how can you comment with code snippets like this? oO

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hehe.. there is a small option at the left end where you can copy the preview link once you click on the line.

lib/block.js Outdated
try {
result = handleTag(request, context, tag);
} catch (error) {
console.error(error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we handle the error here? isn't already wrapped in request-handler in catch statement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove the try-catch. If this throw, error event should be emitted on the stringifier stream and get caught in the request-handler.

lib/block.js Outdated
if (name === 'fragment') {
// Freeze current context & update the indexes
const fragmentContext = context;
context = Object.assign({}, context, {
Copy link
Collaborator

@vigneshshanmugam vigneshshanmugam Aug 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the only changing part here is index, can we have a just update index instead of doing Object overrides.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, not sure here, I pass the context which was there at the time we found the tag further down the line. If I mutate it, fragment might be having some incorrect data inside and then I'd mess up indexes :(

lib/block.js Outdated
result.on('fragment:found', () => {
resultStream.emit('fragment:found');

context = Object.assign({}, context, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

contextPromise
]).then(([ parsedTemplate, context ]) => {
const blockStream = buildBlock(request, Object.assign({
index: 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't allow useland code to update index at any point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, you're right. Thanks for catching!

const handleTag = (request, context, tag) => {
if (tag && tag.name === 'switcher') {
const stream = buildBlock(request, context);
process.nextTick(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it required? buildBlock a sync function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about kinda real-time example when data is being pushed into this "block" sometime later on (but without big impact on performance).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm true, then lets introduce artificial delay and check if back pressure is handled properly.. setTimeout would suffice.

@DeTeam
Copy link
Contributor Author

DeTeam commented Sep 1, 2017

Also, let's rename buildBlock to something like:

  • processTemplate
  • handleTemplate
  • other options?

* template should be parsed into a set of Buffers + Objects

@vigneshshanmugam
Copy link
Collaborator

vigneshshanmugam commented Sep 1, 2017

parseTemplate sounds good to me.(processTemplate)

@DeTeam
Copy link
Contributor Author

DeTeam commented Sep 1, 2017

@vigneshshanmugam we already have parse coming from parse5, it's a bit different, I'd stick to processTemplate

@DeTeam
Copy link
Contributor Author

DeTeam commented Sep 1, 2017

Rename is done, processTemplate is there!

@DeTeam
Copy link
Contributor Author

DeTeam commented Sep 1, 2017

Now I also need to check if it make sense to split context and options again

Timur Amirov added 6 commits September 4, 2017 13:50
* Dynamic context removed (got carried from another experiment)
* Destructive assignment in block.js moved one level above to not be called on every tag
* Tests adjusted
* responseHeaders are used in the way it was used before
* remove unused parameter (addPlaceholders) from tests
* don't check for async (we assume it's always there)
* No new context obj created, mutate index field in te old one
* Clean up commends
* Align property names with the way they are in request handler
* don't allow overriding index field
@DeTeam
Copy link
Contributor Author

DeTeam commented Sep 4, 2017

Rebased. No conflicts. Now going to apply options-context changes.

@@ -42,7 +42,7 @@ describe('Handle tag', () => {
mockHandleTag.returns('');
http.get('http://localhost:8080/template', (response) => {
const request = mockHandleTag.args[0][0];
const tag = mockHandleTag.args[0][1];
const tag = mockHandleTag.args[0][3];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we keep the tag as arg1 .. otherwise it wont be backwards compatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaaaah
good point
yes

resultStream.write({ placeholder: 'async' });
resultStream.end();
});
it('insert pipe definition in the beginning');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we finish the tests for process-template we can go ahead and merge it.. rest looks good to me..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also seem like we're not changing the API much here. Mb minor version update would be suitable.

@zalando zalando deleted a comment from codecov bot Sep 6, 2017
@codecov
Copy link

codecov bot commented Sep 6, 2017

Codecov Report

Merging #173 into master will decrease coverage by 0.04%.
The diff coverage is 96.03%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #173      +/-   ##
==========================================
- Coverage   96.66%   96.61%   -0.05%     
==========================================
  Files          13       14       +1     
  Lines         539      561      +22     
  Branches       93       95       +2     
==========================================
+ Hits          521      542      +21     
- Misses         18       19       +1
Impacted Files Coverage Δ
lib/transform.js 100% <100%> (ø) ⬆️
lib/serializer.js 95.87% <100%> (+0.04%) ⬆️
lib/process-template.js 100% <100%> (ø)
lib/parse-template.js 100% <100%> (ø) ⬆️
lib/request-handler.js 95.04% <93.65%> (-1.11%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0c7de02...10a73dd. Read the comment docs.

const doesMatch = r.test(data);
let message = `No match for the fragmend(index: ${index}, text: ${fragmentText})`;
if (!doesMatch) {
console.log(r, data);
Copy link
Collaborator

@vigneshshanmugam vigneshshanmugam Sep 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we throw error here instead?

@vigneshshanmugam
Copy link
Collaborator

👍

1 similar comment
@DeTeam
Copy link
Contributor Author

DeTeam commented Sep 6, 2017

👍

@vigneshshanmugam vigneshshanmugam merged commit 99ba67e into zalando:master Sep 6, 2017
@vigneshshanmugam vigneshshanmugam deleted the blocks branch September 6, 2017 16:20
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants