Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "modules/fetch/BodyStreamBuffer.h" | 5 #include "modules/fetch/BodyStreamBuffer.h" |
| 6 | 6 |
| 7 #include "bindings/core/v8/ScriptState.h" | |
| 8 #include "bindings/core/v8/V8BindingMacros.h" | |
| 7 #include "core/dom/DOMArrayBuffer.h" | 9 #include "core/dom/DOMArrayBuffer.h" |
| 8 #include "core/dom/DOMTypedArray.h" | 10 #include "core/dom/DOMTypedArray.h" |
| 9 #include "core/dom/ExceptionCode.h" | 11 #include "core/dom/ExceptionCode.h" |
| 12 #include "core/streams/ReadableStreamController.h" | |
| 13 #include "core/streams/ReadableStreamOperations.h" | |
| 14 #include "modules/fetch/Body.h" | |
| 10 #include "modules/fetch/DataConsumerHandleUtil.h" | 15 #include "modules/fetch/DataConsumerHandleUtil.h" |
| 16 #include "platform/RuntimeEnabledFeatures.h" | |
| 11 #include "platform/blob/BlobData.h" | 17 #include "platform/blob/BlobData.h" |
| 12 #include "platform/network/EncodedFormData.h" | 18 #include "platform/network/EncodedFormData.h" |
| 13 | 19 |
| 14 namespace blink { | 20 namespace blink { |
| 15 | 21 |
| 16 class BodyStreamBuffer::LoaderClient final : public GarbageCollectedFinalized<Lo aderClient>, public ActiveDOMObject, public FetchDataLoader::Client { | 22 class BodyStreamBuffer::LoaderClient final : public GarbageCollectedFinalized<Lo aderClient>, public ActiveDOMObject, public FetchDataLoader::Client { |
| 17 WTF_MAKE_NONCOPYABLE(LoaderClient); | 23 WTF_MAKE_NONCOPYABLE(LoaderClient); |
| 18 USING_GARBAGE_COLLECTED_MIXIN(LoaderClient); | 24 USING_GARBAGE_COLLECTED_MIXIN(LoaderClient); |
| 19 public: | 25 public: |
| 20 LoaderClient(ExecutionContext* executionContext, BodyStreamBuffer* buffer, F etchDataLoader::Client* client) | 26 LoaderClient(ExecutionContext* executionContext, BodyStreamBuffer* buffer, F etchDataLoader::Client* client) |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 66 private: | 72 private: |
| 67 void stop() override | 73 void stop() override |
| 68 { | 74 { |
| 69 m_buffer->stopLoading(); | 75 m_buffer->stopLoading(); |
| 70 } | 76 } |
| 71 | 77 |
| 72 Member<BodyStreamBuffer> m_buffer; | 78 Member<BodyStreamBuffer> m_buffer; |
| 73 Member<FetchDataLoader::Client> m_client; | 79 Member<FetchDataLoader::Client> m_client; |
| 74 }; | 80 }; |
| 75 | 81 |
| 76 BodyStreamBuffer::BodyStreamBuffer(PassOwnPtr<FetchDataConsumerHandle> handle) | 82 BodyStreamBuffer::BodyStreamBuffer(ScriptState* scriptState, PassOwnPtr<FetchDat aConsumerHandle> handle) |
| 77 : m_handle(handle) | 83 : UnderlyingSourceBase(scriptState) |
| 84 , m_scriptState(scriptState) | |
| 85 , m_handle(handle) | |
| 78 , m_reader(m_handle->obtainReader(this)) | 86 , m_reader(m_handle->obtainReader(this)) |
| 79 , m_stream(new ReadableByteStream(this, new ReadableByteStream::StrictStrate gy)) | |
| 80 , m_streamNeedsMore(false) | |
| 81 { | 87 { |
| 82 m_stream->didSourceStart(); | 88 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { |
| 89 ScriptState::Scope scope(scriptState); | |
| 90 v8::Local<v8::Object> body = toV8(this, scriptState).As<v8::Object>(); | |
| 91 ASSERT(!body.IsEmpty()); | |
|
Yuki
2016/04/20 12:15:26
This check looks a bit weird.
toV8() may return no
yhirano
2016/04/21 03:41:42
Done.
| |
| 92 | |
| 93 ScriptValue readableStream = ReadableStreamOperations::createReadableStr eam( | |
| 94 scriptState, this, ReadableStreamOperations::createCountQueuingStrat egy(scriptState, 0)); | |
| 95 V8HiddenValue::setHiddenValue(scriptState, body, V8HiddenValue::internal BodyStream(scriptState->isolate()), readableStream.v8Value()); | |
| 96 } else { | |
| 97 m_stream = new ReadableByteStream(this, new ReadableByteStream::StrictSt rategy); | |
| 98 m_stream->didSourceStart(); | |
| 99 } | |
| 83 } | 100 } |
| 84 | 101 |
| 85 PassRefPtr<BlobDataHandle> BodyStreamBuffer::drainAsBlobDataHandle(ExecutionCont ext* executionContext, FetchDataConsumerHandle::Reader::BlobSizePolicy policy) | 102 ScriptValue BodyStreamBuffer::stream() |
| 103 { | |
| 104 ScriptState::Scope scope(m_scriptState.get()); | |
| 105 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 106 v8::Local<v8::Object> body = toV8(this, m_scriptState.get()).As<v8::Obje ct>(); | |
| 107 ASSERT(!body.IsEmpty()); | |
|
Yuki
2016/04/20 12:15:26
Ditto.
yhirano
2016/04/21 03:41:42
Done.
| |
| 108 return ScriptValue(m_scriptState.get(), V8HiddenValue::getHiddenValue(m_ scriptState.get(), body, V8HiddenValue::internalBodyStream(m_scriptState->isolat e()))); | |
| 109 } | |
| 110 return ScriptValue(m_scriptState.get(), toV8(m_stream, m_scriptState.get())) ; | |
| 111 } | |
| 112 | |
| 113 PassRefPtr<BlobDataHandle> BodyStreamBuffer::drainAsBlobDataHandle(FetchDataCons umerHandle::Reader::BlobSizePolicy policy) | |
| 86 { | 114 { |
| 87 ASSERT(!isStreamLocked()); | 115 ASSERT(!isStreamLocked()); |
| 88 ASSERT(!isStreamDisturbed()); | 116 ASSERT(!isStreamDisturbed()); |
| 89 if (isStreamClosed() || isStreamErrored()) | 117 if (isStreamClosed() || isStreamErrored()) |
| 90 return nullptr; | 118 return nullptr; |
| 91 | 119 |
| 92 RefPtr<BlobDataHandle> blobDataHandle = m_reader->drainAsBlobDataHandle(poli cy); | 120 RefPtr<BlobDataHandle> blobDataHandle = m_reader->drainAsBlobDataHandle(poli cy); |
| 93 if (blobDataHandle) { | 121 if (blobDataHandle) { |
| 94 NonThrowableExceptionState exceptionState; | 122 lockAndDisturb(); |
| 95 m_stream->getBytesReader(executionContext, exceptionState); | |
| 96 m_stream->setIsDisturbed(); | |
| 97 close(); | 123 close(); |
| 98 return blobDataHandle.release(); | 124 return blobDataHandle.release(); |
| 99 } | 125 } |
| 100 return nullptr; | 126 return nullptr; |
| 101 } | 127 } |
| 102 | 128 |
| 103 PassRefPtr<EncodedFormData> BodyStreamBuffer::drainAsFormData(ExecutionContext* executionContext) | 129 PassRefPtr<EncodedFormData> BodyStreamBuffer::drainAsFormData() |
| 104 { | 130 { |
| 105 ASSERT(!isStreamLocked()); | 131 ASSERT(!isStreamLocked()); |
| 106 ASSERT(!isStreamDisturbed()); | 132 ASSERT(!isStreamDisturbed()); |
| 107 if (isStreamClosed() || isStreamErrored()) | 133 if (isStreamClosed() || isStreamErrored()) |
| 108 return nullptr; | 134 return nullptr; |
| 109 | 135 |
| 110 RefPtr<EncodedFormData> formData = m_reader->drainAsFormData(); | 136 RefPtr<EncodedFormData> formData = m_reader->drainAsFormData(); |
| 111 if (formData) { | 137 if (formData) { |
| 112 NonThrowableExceptionState exceptionState; | 138 lockAndDisturb(); |
| 113 m_stream->getBytesReader(executionContext, exceptionState); | |
| 114 m_stream->setIsDisturbed(); | |
| 115 close(); | 139 close(); |
| 116 return formData.release(); | 140 return formData.release(); |
| 117 } | 141 } |
| 118 return nullptr; | 142 return nullptr; |
| 119 } | 143 } |
| 120 | 144 |
| 121 PassOwnPtr<FetchDataConsumerHandle> BodyStreamBuffer::releaseHandle(ExecutionCon text* executionContext) | 145 PassOwnPtr<FetchDataConsumerHandle> BodyStreamBuffer::releaseHandle() |
| 122 { | 146 { |
| 123 ASSERT(!isStreamLocked()); | 147 ASSERT(!isStreamLocked()); |
| 124 ASSERT(!isStreamDisturbed()); | 148 ASSERT(!isStreamDisturbed()); |
| 125 m_reader = nullptr; | 149 lockAndDisturb(); |
| 126 m_stream->setIsDisturbed(); | |
| 127 NonThrowableExceptionState exceptionState; | |
| 128 m_stream->getBytesReader(executionContext, exceptionState); | |
| 129 | 150 |
| 130 if (isStreamClosed()) | 151 if (isStreamClosed()) |
| 131 return createFetchDataConsumerHandleFromWebHandle(createDoneDataConsumer Handle()); | 152 return createFetchDataConsumerHandleFromWebHandle(createDoneDataConsumer Handle()); |
| 132 if (isStreamErrored()) | 153 if (isStreamErrored()) |
| 133 return createFetchDataConsumerHandleFromWebHandle(createUnexpectedErrorD ataConsumerHandle()); | 154 return createFetchDataConsumerHandleFromWebHandle(createUnexpectedErrorD ataConsumerHandle()); |
| 134 | 155 |
| 135 ASSERT(m_handle); | 156 ASSERT(m_handle); |
| 136 OwnPtr<FetchDataConsumerHandle> handle = m_handle.release(); | 157 OwnPtr<FetchDataConsumerHandle> handle = m_handle.release(); |
| 137 close(); | 158 close(); |
| 138 return handle.release(); | 159 return handle.release(); |
| 139 } | 160 } |
| 140 | 161 |
| 141 void BodyStreamBuffer::startLoading(ExecutionContext* executionContext, FetchDat aLoader* loader, FetchDataLoader::Client* client) | 162 void BodyStreamBuffer::startLoading(FetchDataLoader* loader, FetchDataLoader::Cl ient* client) |
| 142 { | 163 { |
| 143 ASSERT(!m_loader); | 164 ASSERT(!m_loader); |
| 144 OwnPtr<FetchDataConsumerHandle> handle = releaseHandle(executionContext); | 165 OwnPtr<FetchDataConsumerHandle> handle = releaseHandle(); |
| 145 m_loader = loader; | 166 m_loader = loader; |
| 146 loader->start(handle.get(), new LoaderClient(executionContext, this, client) ); | 167 loader->start(handle.get(), new LoaderClient(m_scriptState->getExecutionCont ext(), this, client)); |
| 147 } | 168 } |
| 148 | 169 |
| 149 bool BodyStreamBuffer::hasPendingActivity() const | 170 bool BodyStreamBuffer::hasPendingActivity() const |
| 150 { | 171 { |
| 151 return m_loader || (isStreamLocked() && isStreamReadable()); | 172 if (m_loader) |
| 173 return true; | |
| 174 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) | |
| 175 return UnderlyingSourceBase::hasPendingActivity(); | |
| 176 | |
| 177 return m_stream->stateInternal() == ReadableStream::Readable && m_stream->is Locked(); | |
| 152 } | 178 } |
| 153 | 179 |
| 154 void BodyStreamBuffer::stop() | 180 void BodyStreamBuffer::stop() |
| 155 { | 181 { |
| 156 m_reader = nullptr; | 182 m_reader = nullptr; |
| 157 m_handle = nullptr; | 183 m_handle = nullptr; |
| 158 } | 184 } |
| 159 | 185 |
| 160 void BodyStreamBuffer::pullSource() | 186 void BodyStreamBuffer::pullSource() |
| 161 { | 187 { |
| 162 ASSERT(!m_streamNeedsMore); | 188 ASSERT(!m_streamNeedsMore); |
| 163 m_streamNeedsMore = true; | 189 m_streamNeedsMore = true; |
| 164 processData(); | 190 processData(); |
| 165 } | 191 } |
| 166 | 192 |
| 167 ScriptPromise BodyStreamBuffer::cancelSource(ScriptState* scriptState, ScriptVal ue) | 193 ScriptPromise BodyStreamBuffer::cancelSource(ScriptState* scriptState, ScriptVal ue) |
| 168 { | 194 { |
| 195 ASSERT(scriptState == m_scriptState.get()); | |
| 169 close(); | 196 close(); |
| 170 return ScriptPromise::castUndefined(scriptState); | 197 return ScriptPromise::castUndefined(scriptState); |
| 171 } | 198 } |
| 199 | |
| 200 ScriptPromise BodyStreamBuffer::pull(ScriptState* scriptState) | |
| 201 { | |
| 202 ASSERT(!m_streamNeedsMore); | |
| 203 ASSERT(scriptState == m_scriptState.get()); | |
| 204 m_streamNeedsMore = true; | |
| 205 processData(); | |
| 206 return ScriptPromise::castUndefined(scriptState); | |
| 207 } | |
| 208 | |
| 209 ScriptPromise BodyStreamBuffer::cancel(ScriptState* scriptState, ScriptValue rea son) | |
| 210 { | |
| 211 ASSERT(scriptState == m_scriptState.get()); | |
| 212 close(); | |
| 213 return ScriptPromise::castUndefined(scriptState); | |
| 214 } | |
| 172 | 215 |
| 173 void BodyStreamBuffer::didGetReadable() | 216 void BodyStreamBuffer::didGetReadable() |
| 174 { | 217 { |
| 175 if (!m_reader) | 218 if (!m_reader) |
| 176 return; | 219 return; |
| 177 | 220 |
| 178 if (!m_streamNeedsMore) { | 221 if (!m_streamNeedsMore) { |
| 179 // Perform zero-length read to call close()/error() early. | 222 // Perform zero-length read to call close()/error() early. |
| 180 size_t readSize; | 223 size_t readSize; |
| 181 WebDataConsumerHandle::Result result = m_reader->read(nullptr, 0, WebDat aConsumerHandle::FlagNone, &readSize); | 224 WebDataConsumerHandle::Result result = m_reader->read(nullptr, 0, WebDat aConsumerHandle::FlagNone, &readSize); |
| 182 switch (result) { | 225 switch (result) { |
| 183 case WebDataConsumerHandle::Ok: | 226 case WebDataConsumerHandle::Ok: |
| 184 case WebDataConsumerHandle::ShouldWait: | 227 case WebDataConsumerHandle::ShouldWait: |
| 185 return; | 228 return; |
| 186 case WebDataConsumerHandle::Done: | 229 case WebDataConsumerHandle::Done: |
| 187 close(); | 230 close(); |
| 188 return; | 231 return; |
| 189 case WebDataConsumerHandle::Busy: | 232 case WebDataConsumerHandle::Busy: |
| 190 case WebDataConsumerHandle::ResourceExhausted: | 233 case WebDataConsumerHandle::ResourceExhausted: |
| 191 case WebDataConsumerHandle::UnexpectedError: | 234 case WebDataConsumerHandle::UnexpectedError: |
| 192 error(); | 235 error(); |
| 193 return; | 236 return; |
| 194 } | 237 } |
| 195 return; | 238 return; |
| 196 } | 239 } |
| 197 processData(); | 240 processData(); |
| 198 } | 241 } |
| 199 | 242 |
| 200 bool BodyStreamBuffer::isStreamReadable() const | 243 bool BodyStreamBuffer::isStreamReadable() |
| 201 { | 244 { |
| 245 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 246 ScriptState::Scope scope(m_scriptState.get()); | |
| 247 return ReadableStreamOperations::isReadable(m_scriptState.get(), stream( )); | |
| 248 } | |
| 202 return m_stream->stateInternal() == ReadableStream::Readable; | 249 return m_stream->stateInternal() == ReadableStream::Readable; |
| 203 } | 250 } |
| 204 | 251 |
| 205 bool BodyStreamBuffer::isStreamClosed() const | 252 bool BodyStreamBuffer::isStreamClosed() |
| 206 { | 253 { |
| 254 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 255 ScriptState::Scope scope(m_scriptState.get()); | |
| 256 return ReadableStreamOperations::isClosed(m_scriptState.get(), stream()) ; | |
| 257 } | |
| 207 return m_stream->stateInternal() == ReadableStream::Closed; | 258 return m_stream->stateInternal() == ReadableStream::Closed; |
| 208 } | 259 } |
| 209 | 260 |
| 210 bool BodyStreamBuffer::isStreamErrored() const | 261 bool BodyStreamBuffer::isStreamErrored() |
| 211 { | 262 { |
| 263 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 264 ScriptState::Scope scope(m_scriptState.get()); | |
| 265 return ReadableStreamOperations::isErrored(m_scriptState.get(), stream() ); | |
| 266 } | |
| 212 return m_stream->stateInternal() == ReadableStream::Errored; | 267 return m_stream->stateInternal() == ReadableStream::Errored; |
| 213 } | 268 } |
| 214 | 269 |
| 215 bool BodyStreamBuffer::isStreamLocked() const | 270 bool BodyStreamBuffer::isStreamLocked() |
| 216 { | 271 { |
| 272 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 273 ScriptState::Scope scope(m_scriptState.get()); | |
| 274 return ReadableStreamOperations::isLocked(m_scriptState.get(), stream()) ; | |
| 275 } | |
| 217 return m_stream->isLocked(); | 276 return m_stream->isLocked(); |
| 218 } | 277 } |
| 219 | 278 |
| 220 bool BodyStreamBuffer::isStreamDisturbed() const | 279 bool BodyStreamBuffer::isStreamDisturbed() |
| 221 { | 280 { |
| 281 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 282 ScriptState::Scope scope(m_scriptState.get()); | |
| 283 return ReadableStreamOperations::isDisturbed(m_scriptState.get(), stream ()); | |
| 284 } | |
| 222 return m_stream->isDisturbed(); | 285 return m_stream->isDisturbed(); |
| 223 } | 286 } |
| 224 | 287 |
| 288 void BodyStreamBuffer::setDisturbed() | |
| 289 { | |
| 290 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 291 ScriptState::Scope scope(m_scriptState.get()); | |
| 292 ReadableStreamOperations::setDisturbed(m_scriptState.get(), stream()); | |
| 293 } else { | |
| 294 m_stream->setIsDisturbed(); | |
| 295 } | |
| 296 } | |
| 297 | |
| 298 void BodyStreamBuffer::lockAndDisturb() | |
| 299 { | |
| 300 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { | |
| 301 ScriptState::Scope scope(m_scriptState.get()); | |
| 302 NonThrowableExceptionState exceptionState; | |
| 303 ReadableStreamOperations::getReader(m_scriptState.get(), stream(), excep tionState); | |
| 304 ReadableStreamOperations::setDisturbed(m_scriptState.get(), stream()); | |
| 305 } else { | |
| 306 NonThrowableExceptionState exceptionState; | |
| 307 m_stream->getBytesReader(m_scriptState->getExecutionContext(), exception State); | |
| 308 m_stream->setIsDisturbed(); | |
| 309 } | |
| 310 } | |
| 311 | |
| 225 void BodyStreamBuffer::close() | 312 void BodyStreamBuffer::close() |
| 226 { | 313 { |
| 314 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) | |
| 315 controller()->close(); | |
| 316 else | |
| 317 m_stream->close(); | |
| 227 m_reader = nullptr; | 318 m_reader = nullptr; |
| 228 m_stream->close(); | 319 m_handle = nullptr; |
| 229 m_handle.clear(); | |
| 230 } | 320 } |
| 231 | 321 |
| 232 void BodyStreamBuffer::error() | 322 void BodyStreamBuffer::error() |
| 233 { | 323 { |
| 324 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) | |
| 325 controller()->error(DOMException::create(NetworkError, "network error")) ; | |
| 326 else | |
| 327 m_stream->error(DOMException::create(NetworkError, "network error")); | |
| 234 m_reader = nullptr; | 328 m_reader = nullptr; |
| 235 m_stream->error(DOMException::create(NetworkError, "network error")); | 329 m_handle = nullptr; |
| 236 m_handle.clear(); | |
| 237 } | 330 } |
| 238 | 331 |
| 239 void BodyStreamBuffer::processData() | 332 void BodyStreamBuffer::processData() |
| 240 { | 333 { |
| 241 ASSERT(m_reader); | 334 ASSERT(m_reader); |
| 242 while (m_streamNeedsMore) { | 335 while (m_streamNeedsMore) { |
| 243 const void* buffer; | 336 const void* buffer; |
| 244 size_t available; | 337 size_t available; |
| 245 WebDataConsumerHandle::Result result = m_reader->beginRead(&buffer, WebD ataConsumerHandle::FlagNone, &available); | 338 WebDataConsumerHandle::Result result = m_reader->beginRead(&buffer, WebD ataConsumerHandle::FlagNone, &available); |
| 246 switch (result) { | 339 switch (result) { |
| 247 case WebDataConsumerHandle::Ok: | 340 case WebDataConsumerHandle::Ok: |
| 248 m_streamNeedsMore = m_stream->enqueue(DOMUint8Array::create(static_c ast<const unsigned char*>(buffer), available)); | 341 if (RuntimeEnabledFeatures::responseBodyWithV8ExtraStreamEnabled()) { |
| 342 controller()->enqueue(DOMUint8Array::create(static_cast<const un signed char*>(buffer), available)); | |
| 343 m_streamNeedsMore = controller()->desiredSize() > 0; | |
| 344 } else { | |
| 345 m_streamNeedsMore = m_stream->enqueue(DOMUint8Array::create(stat ic_cast<const unsigned char*>(buffer), available)); | |
| 346 } | |
| 249 m_reader->endRead(available); | 347 m_reader->endRead(available); |
| 250 break; | 348 break; |
| 251 | 349 |
| 252 case WebDataConsumerHandle::Done: | 350 case WebDataConsumerHandle::Done: |
| 253 close(); | 351 close(); |
| 254 return; | 352 return; |
| 255 | 353 |
| 256 case WebDataConsumerHandle::ShouldWait: | 354 case WebDataConsumerHandle::ShouldWait: |
| 257 return; | 355 return; |
| 258 | 356 |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 273 | 371 |
| 274 void BodyStreamBuffer::stopLoading() | 372 void BodyStreamBuffer::stopLoading() |
| 275 { | 373 { |
| 276 if (!m_loader) | 374 if (!m_loader) |
| 277 return; | 375 return; |
| 278 m_loader->cancel(); | 376 m_loader->cancel(); |
| 279 m_loader = nullptr; | 377 m_loader = nullptr; |
| 280 } | 378 } |
| 281 | 379 |
| 282 } // namespace blink | 380 } // namespace blink |
| OLD | NEW |