blob: b1db33da7a37da90b1e50a2fc77ccbc35e8757a6 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Queue implementation used by ReadableStream and WritableStream.
(function(global, binding, v8) {
'use strict';
const _front = v8.createPrivateSymbol('front');
const _back = v8.createPrivateSymbol('back');
const _cursor = v8.createPrivateSymbol('cursor');
const _size = v8.createPrivateSymbol('size');
const _elements = v8.createPrivateSymbol('elements');
const _next = v8.createPrivateSymbol('next');
// Take copies of global objects to protect against them being replaced.
const RangeError = global.RangeError;
// shift() and peek() can only be called on a non-empty queue. This function
// throws an exception with the message mentioning |functionName| if |queue|
// is empty.
function requireNonEmptyQueue(queue, functionName) {
if (queue[_size] === 0) {
throw new RangeError(
`${functionName}() must not be called on an empty queue`);
}
}
// Simple queue structure. Avoids scalability issues with using
// InternalPackedArray directly by using multiple arrays in a linked list and
// keeping the array size bounded.
const QUEUE_MAX_ARRAY_SIZE = 16384;
class SimpleQueue {
constructor() {
// [_front] and [_back] are always defined.
this[_front] = {
[_elements]: new v8.InternalPackedArray(),
[_next]: undefined,
};
this[_back] = this[_front];
// The cursor is used to avoid calling InternalPackedArray.shift(). It
// contains the index of the front element of the array inside the
// frontmost node. It is always in the range [0, QUEUE_MAX_ARRAY_SIZE).
this[_cursor] = 0;
// When there is only one node, size === elements.length - cursor.
this[_size] = 0;
}
get length() {
return this[_size];
}
// For exception safety, this method is structured in order:
// 1. Read state
// 2. Calculate required state mutations
// 3. Perform state mutations
push(element) {
const oldBack = this[_back];
let newBack = oldBack;
// assert(oldBack[_next] === undefined);
if (oldBack[_elements].length === QUEUE_MAX_ARRAY_SIZE - 1) {
newBack = {
[_elements]: new v8.InternalPackedArray(),
[_next]: undefined,
};
}
// push() is the mutation most likely to throw an exception, so it
// goes first.
oldBack[_elements].push(element);
if (newBack !== oldBack) {
this[_back] = newBack;
oldBack[_next] = newBack;
}
++this[_size];
}
// Like push(), shift() follows the read -> calculate -> mutate pattern for
// exception safety.
shift() {
requireNonEmptyQueue(this, 'shift');
const oldFront = this[_front];
let newFront = oldFront;
const oldCursor = this[_cursor];
let newCursor = oldCursor + 1;
const elements = oldFront[_elements];
const element = elements[oldCursor];
if (newCursor === QUEUE_MAX_ARRAY_SIZE) {
// assert(elements.length === QUEUE_MAX_ARRAY_SIZE);
// assert(oldFront[_next] !== undefined);
newFront = oldFront[_next];
newCursor = 0;
}
// No mutations before this point.
--this[_size];
this[_cursor] = newCursor;
if (oldFront !== newFront) {
this[_front] = newFront;
}
// Permit shifted element to be garbage collected.
elements[oldCursor] = undefined;
return element;
}
// The tricky thing about forEach() is that it can be called
// re-entrantly. The queue may be mutated inside the callback. It is easy to
// see that push() within the callback has no negative effects since the end
// of the queue is checked for on every iteration. If shift() is called
// repeatedly within the callback then the next iteration may return an
// element that has been removed. In this case the callback will be called
// with undefined values until we either "catch up" with elements that still
// exist or reach the back of the queue.
forEach(callback) {
let i = this[_cursor];
let node = this[_front];
let elements = node[_elements];
while (i !== elements.length || node[_next] !== undefined) {
if (i === elements.length) {
// assert(node[_next] !== undefined);
// assert(i === QUEUE_MAX_ARRAY_SIZE);
node = node[_next];
elements = node[_elements];
i = 0;
if (elements.length === 0) {
break;
}
}
callback(elements[i]);
++i;
}
}
// Return the element that would be returned if shift() was called now,
// without modifying the queue.
peek() {
requireNonEmptyQueue(this, 'peek');
const front = this[_front];
const cursor = this[_cursor];
return front[_elements][cursor];
}
}
binding.SimpleQueue = SimpleQueue;
});