1 | 'use strict';
|
2 |
|
3 | const ContentLengthStream = require('./streams/content-length-stream');
|
4 | const EventEmitter = require('events').EventEmitter;
|
5 | const PassThrough = require('stream').PassThrough;
|
6 | const LinkHeader = require('http-link-header');
|
7 | const zlib = require('zlib');
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const getAttributes = (tag, context) => {
|
17 | const attributes = Object.assign({}, tag.attributes);
|
18 | const fragmentId = attributes.id;
|
19 | if (context && fragmentId && context[fragmentId]) {
|
20 | const fragmentCtxt = context[fragmentId];
|
21 | Object.assign(attributes, fragmentCtxt);
|
22 | }
|
23 |
|
24 | return {
|
25 | url: attributes.src,
|
26 | id: fragmentId,
|
27 | fallbackUrl: attributes['fallback-src'],
|
28 | timeout: parseInt(attributes.timeout || 3000, 10),
|
29 | async: attributes.async,
|
30 | primary: attributes.primary,
|
31 | public: attributes.public
|
32 | };
|
33 | };
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | module.exports = class Fragment extends EventEmitter {
|
40 | |
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | constructor(
|
49 | {
|
50 | tag,
|
51 | context,
|
52 | index,
|
53 | requestFragment,
|
54 | pipeInstanceName,
|
55 | maxAssetLinks,
|
56 | pipeAttributes = () => {}
|
57 | } = {}
|
58 | ) {
|
59 | super();
|
60 | this.attributes = getAttributes(tag, context, index);
|
61 | ['async', 'primary', 'public'].forEach(key => {
|
62 | let value = this.attributes[key];
|
63 | if (value || value === '') {
|
64 | value = true;
|
65 | } else {
|
66 | value = false;
|
67 | }
|
68 | this.attributes[key] = value;
|
69 | });
|
70 |
|
71 | this.index = index;
|
72 | this.maxAssetLinks = maxAssetLinks;
|
73 | this.getPipeAttributes = () =>
|
74 | pipeAttributes(
|
75 | Object.assign({}, { id: this.index }, tag.attributes)
|
76 | );
|
77 | this.requestFragment = requestFragment;
|
78 | this.pipeInstanceName = pipeInstanceName;
|
79 | this.stream = new PassThrough();
|
80 | }
|
81 |
|
82 | |
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 | fetch(request, isFallback) {
|
89 | if (!isFallback) {
|
90 | this.emit('start');
|
91 | }
|
92 | const url = isFallback
|
93 | ? this.attributes.fallbackUrl
|
94 | : this.attributes.url;
|
95 |
|
96 | this.requestFragment(url, this.attributes, request).then(
|
97 | res => this.onResponse(res, isFallback),
|
98 | err => {
|
99 | if (!isFallback) {
|
100 | const { fallbackUrl } = this.attributes;
|
101 | if (fallbackUrl) {
|
102 | this.emit('fallback', err);
|
103 | this.fetch(request, true);
|
104 | } else {
|
105 | this.emit('error', err);
|
106 | this.stream.end();
|
107 | }
|
108 | } else {
|
109 | this.stream.end();
|
110 | }
|
111 | }
|
112 | );
|
113 |
|
114 | if (this.attributes.async) {
|
115 | return Buffer.from(
|
116 | `<script data-pipe>${this.pipeInstanceName}.placeholder(${this
|
117 | .index})</script>`
|
118 | );
|
119 | }
|
120 | return this.stream;
|
121 | }
|
122 |
|
123 | |
124 |
|
125 |
|
126 |
|
127 |
|
128 | onResponse(response, isFallback) {
|
129 | const { statusCode, headers } = response;
|
130 |
|
131 | if (!isFallback) {
|
132 | this.emit('response', statusCode, headers);
|
133 | }
|
134 |
|
135 | const { refs } = LinkHeader.parse(
|
136 | [headers.link, headers['x-amz-meta-link']].join(',')
|
137 | );
|
138 |
|
139 | this.scriptRefs = refs
|
140 | .filter(ref => ref.rel === 'fragment-script')
|
141 | .slice(0, this.maxAssetLinks)
|
142 | .map(ref => ref.uri);
|
143 | this.styleRefs = refs
|
144 | .filter(ref => ref.rel === 'stylesheet')
|
145 | .slice(0, this.maxAssetLinks)
|
146 | .map(ref => ref.uri);
|
147 |
|
148 | this.insertStart();
|
149 |
|
150 | const contentLengthStream = new ContentLengthStream(contentLength => {
|
151 | if (!isFallback) {
|
152 | this.emit('end', contentLength);
|
153 | }
|
154 | });
|
155 |
|
156 | contentLengthStream.on('end', () => {
|
157 | this.insertEnd();
|
158 | this.stream.end();
|
159 | });
|
160 |
|
161 | const handleError = err => {
|
162 | this.emit('warn', err);
|
163 | contentLengthStream.end();
|
164 | };
|
165 |
|
166 |
|
167 | response.on('error', handleError);
|
168 | contentLengthStream.on('error', handleError);
|
169 |
|
170 |
|
171 | let responseStream = response;
|
172 | const contentEncoding = headers['content-encoding'];
|
173 | if (
|
174 | contentEncoding &&
|
175 | (contentEncoding === 'gzip' || contentEncoding === 'deflate')
|
176 | ) {
|
177 | let unzipStream = zlib.createUnzip();
|
178 | unzipStream.on('error', handleError);
|
179 | responseStream = response.pipe(unzipStream);
|
180 | }
|
181 |
|
182 | responseStream
|
183 | .pipe(contentLengthStream)
|
184 | .pipe(this.stream, { end: false });
|
185 | }
|
186 |
|
187 | |
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 | insertStart() {
|
194 | this.styleRefs.forEach(uri => {
|
195 | this.stream.write(
|
196 | this.attributes.async
|
197 | ? `<script>${this
|
198 | .pipeInstanceName}.loadCSS("${uri}")</script>`
|
199 | : `<link rel="stylesheet" href="${uri}">`
|
200 | );
|
201 | });
|
202 |
|
203 | if (this.scriptRefs.length === 0) {
|
204 | this.stream.write(
|
205 | `<script data-pipe>${this.pipeInstanceName}.start(${this
|
206 | .index})</script>`
|
207 | );
|
208 | this.index++;
|
209 | return;
|
210 | }
|
211 |
|
212 | const range = [this.index, this.index + this.scriptRefs.length - 1];
|
213 | const fragmentId = this.attributes.id || range[0];
|
214 | this.scriptRefs.forEach(uri => {
|
215 | const attributes = Object.assign({}, this.getPipeAttributes(), {
|
216 | id: fragmentId,
|
217 | range
|
218 | });
|
219 | this.stream.write(
|
220 | `<script data-pipe>${this.pipeInstanceName}.start(${this
|
221 | .index}, "${uri}", ${JSON.stringify(attributes)})</script>`
|
222 | );
|
223 | this.index++;
|
224 | });
|
225 | }
|
226 |
|
227 | |
228 |
|
229 |
|
230 | insertEnd() {
|
231 | if (this.scriptRefs.length > 0) {
|
232 | const range = [this.index - this.scriptRefs.length, this.index - 1];
|
233 | this.index--;
|
234 | const fragmentId = this.attributes.id || range[0];
|
235 | this.scriptRefs.reverse().forEach(uri => {
|
236 | const attributes = Object.assign({}, this.getPipeAttributes(), {
|
237 | id: fragmentId,
|
238 | range
|
239 | });
|
240 | this.stream.write(
|
241 | `<script data-pipe>${this.pipeInstanceName}.end(${this
|
242 | .index--}, "${uri}", ${JSON.stringify(
|
243 | attributes
|
244 | )})</script>`
|
245 | );
|
246 | });
|
247 | } else {
|
248 | this.stream.write(
|
249 | `<script data-pipe>${this.pipeInstanceName}.end(${this.index -
|
250 | 1})</script>`
|
251 | );
|
252 | }
|
253 | }
|
254 | };
|