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
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Queue of pending requests from an origin.
7 *
8 */
9'use strict';
10
11/**
12 * Represents a queued request. Once given a token, call complete() once the
13 * request is processed (or dropped.)
14 * @interface
15 */
16function QueuedRequestToken() {}
17
18/** Completes (or cancels) this queued request. */
19QueuedRequestToken.prototype.complete = function() {};
20
21/**
22 * @param {!RequestQueue} queue The queue for this request.
23 * @param {function(QueuedRequestToken)} beginCb Called when work may begin on
24 *     this request.
25 * @param {RequestToken} opt_prev Previous request in the same queue.
26 * @param {RequestToken} opt_next Next request in the same queue.
27 * @constructor
28 * @implements {QueuedRequestToken}
29 */
30function RequestToken(queue, beginCb, opt_prev, opt_next) {
31  /** @private {!RequestQueue} */
32  this.queue_ = queue;
33  /** @type {function(QueuedRequestToken)} */
34  this.beginCb = beginCb;
35  /** @type {RequestToken} */
36  this.prev = null;
37  /** @type {RequestToken} */
38  this.next = null;
39  /** @private {boolean} */
40  this.completed_ = false;
41}
42
43/** Completes (or cancels) this queued request. */
44RequestToken.prototype.complete = function() {
45  if (this.completed_) {
46    // Either the caller called us more than once, or the timer is firing.
47    // Either way, nothing more to do here.
48    return;
49  }
50  this.completed_ = true;
51  this.queue_.complete(this);
52};
53
54/** @return {boolean} Whether this token has already completed. */
55RequestToken.prototype.completed = function() {
56  return this.completed_;
57};
58
59/**
60 * @constructor
61 */
62function RequestQueue() {
63  /** @private {RequestToken} */
64  this.head_ = null;
65  /** @private {RequestToken} */
66  this.tail_ = null;
67}
68
69/**
70 * Inserts this token into the queue.
71 * @param {RequestToken} token Queue token
72 * @private
73 */
74RequestQueue.prototype.insertToken_ = function(token) {
75  if (this.head_ === null) {
76    this.head_ = token;
77    this.tail_ = token;
78  } else {
79    if (!this.tail_) throw 'Non-empty list missing tail';
80    this.tail_.next = token;
81    token.prev = this.tail_;
82    this.tail_ = token;
83  }
84};
85
86/**
87 * Removes this token from the queue.
88 * @param {RequestToken} token Queue token
89 * @private
90 */
91RequestQueue.prototype.removeToken_ = function(token) {
92  if (token.next) {
93    token.next.prev = token.prev;
94  }
95  if (token.prev) {
96    token.prev.next = token.next;
97  }
98  if (this.head_ === token && this.tail_ === token) {
99    this.head_ = this.tail_ = null;
100  } else {
101    if (this.head_ === token) {
102      this.head_ = token.next;
103      this.head_.prev = null;
104    }
105    if (this.tail_ === token) {
106      this.tail_ = token.prev;
107      this.tail_.next = null;
108    }
109  }
110  token.prev = token.next = null;
111};
112
113/**
114 * Completes this token's request, and begins the next queued request, if one
115 * exists.
116 * @param {RequestToken} token Queue token
117 */
118RequestQueue.prototype.complete = function(token) {
119  var next = token.next;
120  this.removeToken_(token);
121  if (next) {
122    next.beginCb(next);
123  }
124};
125
126/** @return {boolean} Whether this queue is empty. */
127RequestQueue.prototype.empty = function() {
128  return this.head_ === null;
129};
130
131/**
132 * Queues this request, and, if it's the first request, begins work on it.
133 * @param {function(QueuedRequestToken)} beginCb Called when work begins on this
134 *     request.
135 * @param {Countdown} timer Countdown timer
136 * @return {QueuedRequestToken} A token for the request.
137 */
138RequestQueue.prototype.queueRequest = function(beginCb, timer) {
139  var startNow = this.empty();
140  var token = new RequestToken(this, beginCb);
141  // Clone the timer to set a callback on it, which will ensure complete() is
142  // eventually called, even if the caller never gets around to it.
143  timer.clone(token.complete.bind(token));
144  this.insertToken_(token);
145  if (startNow) {
146    window.setTimeout(function() {
147      if (!token.completed()) {
148        token.beginCb(token);
149      }
150    }, 0);
151  }
152  return token;
153};
154
155/**
156 * @constructor
157 */
158function OriginKeyedRequestQueue() {
159  /** @private {Object.<string, !RequestQueue>} */
160  this.requests_ = {};
161}
162
163/**
164 * Queues this request, and, if it's the first request, begins work on it.
165 * @param {string} appId Application Id
166 * @param {string} origin Request origin
167 * @param {function(QueuedRequestToken)} beginCb Called when work begins on this
168 *     request.
169 * @param {Countdown} timer Countdown timer
170 * @return {QueuedRequestToken} A token for the request.
171 */
172OriginKeyedRequestQueue.prototype.queueRequest =
173    function(appId, origin, beginCb, timer) {
174  var key = appId + origin;
175  if (!this.requests_.hasOwnProperty(key)) {
176    this.requests_[key] = new RequestQueue();
177  }
178  var queue = this.requests_[key];
179  return queue.queueRequest(beginCb, timer);
180};
181