UNPKG

react-relay

Version:

A framework for building data-driven React applications.

626 lines (546 loc) • 19 kB
/** * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule GraphQLSegment * @typechecks */ 'use strict'; var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default']; var _slicedToArray = require('babel-runtime/helpers/sliced-to-array')['default']; var _Object$assign = require('babel-runtime/core-js/object/assign')['default']; var _Object$keys = require('babel-runtime/core-js/object/keys')['default']; var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler'); /** * Represents one contiguous segment of edges within a `GraphQLRange`. Has * methods for adding/removing edges (`appendEdge`, `prependEdge`, `removeEdge`) * and working with cursors (`getFirstCursor`, `getLastCursor` etc) * * Edges are never actually deleted from segments; they are merely marked as * being deleted. As such, `GraphQLSegment` offers both a `getCount` method * (returning the number of non-deleted edges) and a `getLength` method (which * returns the total number, including deleted edges). * * Used mostly as an implementation detail internal to `GraphQLRange`. * * @internal */ var GraphQLSegment = (function () { function GraphQLSegment() { _classCallCheck(this, GraphQLSegment); // We use a map rather than an array because indices can become negative // when prepending. this._indexToMetadataMap = {}; // We keep track of past indices to ensure we can delete them completely. this._idToIndicesMap = {}; this._cursorToIndexMap = {}; this._count = 0; this._minIndex = null; this._maxIndex = null; } /** * @param {string} cursor * @return {?number} */ GraphQLSegment.prototype._getIndexForCursor = function _getIndexForCursor(cursor) { return this._cursorToIndexMap[cursor]; }; /** * @param {string} id * @return {?number} */ GraphQLSegment.prototype._getIndexForID = function _getIndexForID(id) { var indices = this._idToIndicesMap[id]; return indices && indices[0]; }; /** * @return {?string} cursor for first non-deleted edge */ GraphQLSegment.prototype.getFirstCursor = function getFirstCursor() { if (this.getLength()) { for (var ii = this._minIndex; ii <= this._maxIndex; ii++) { var metadata = this._indexToMetadataMap[ii]; if (!metadata.deleted) { return metadata.cursor; } } } }; /** * @return {?string} cursor for last non-deleted edge */ GraphQLSegment.prototype.getLastCursor = function getLastCursor() { if (this.getLength()) { for (var ii = this._maxIndex; ii >= this._minIndex; ii--) { var metadata = this._indexToMetadataMap[ii]; if (!metadata.deleted) { return metadata.cursor; } } } }; /** * @return {?string} id for first non-deleted edge */ GraphQLSegment.prototype.getFirstID = function getFirstID() { if (this.getLength()) { for (var ii = this._minIndex; ii <= this._maxIndex; ii++) { var metadata = this._indexToMetadataMap[ii]; if (!metadata.deleted) { return metadata.edgeID; } } } }; /** * @return {?string} id for last non-deleted edge */ GraphQLSegment.prototype.getLastID = function getLastID() { if (this.getLength()) { for (var ii = this._maxIndex; ii >= this._minIndex; ii--) { var metadata = this._indexToMetadataMap[ii]; if (!metadata.deleted) { return metadata.edgeID; } } } }; /** * @param {number} index * @return {?object} Returns the not-deleted edge at index */ GraphQLSegment.prototype._getEdgeAtIndex = function _getEdgeAtIndex(index) { var edge = this._indexToMetadataMap[index]; return edge && !edge.deleted ? edge : null; }; /** * Returns whether there is a non-deleted edge for id * @param {string} id * @return {boolean} */ GraphQLSegment.prototype.containsEdgeWithID = function containsEdgeWithID(id) { var index = this._getIndexForID(id); if (index === undefined) { return false; } return !!this._getEdgeAtIndex(index); }; /** * Returns whether there is a non-deleted edge for cursor * @param {string} cursor * @return {boolean} */ GraphQLSegment.prototype.containsEdgeWithCursor = function containsEdgeWithCursor(cursor) { var index = this._getIndexForCursor(cursor); if (index === undefined) { return false; } return !!this._getEdgeAtIndex(index); }; /** * Returns up to count number of ids and cursors that is after input cursor * @param {number} count * @param {?string} cursor * @return {object} object with arrays of ids and cursors */ GraphQLSegment.prototype.getMetadataAfterCursor = function getMetadataAfterCursor(count, cursor) { if (!this.getLength()) { return { edgeIDs: [], cursors: [] }; } var currentIndex = this._minIndex; if (cursor) { var index = this._getIndexForCursor(cursor); if (index === undefined) { console.error('This segment does not have a cursor %s', cursor); return { edgeIDs: [], cursors: [] }; } currentIndex = index + 1; } var total = 0; var edgeIDs = []; var cursors = []; while (currentIndex <= this._maxIndex && total < count) { var metadata = this._indexToMetadataMap[currentIndex]; if (!metadata.deleted) { edgeIDs.push(metadata.edgeID); cursors.push(metadata.cursor); total++; } currentIndex++; } return { edgeIDs: edgeIDs, cursors: cursors }; }; /** * Returns up to count number of ids and cursors that is before index * @param {number} count * @param {?string} cursor * @return {object} object with arrays of ids and cursors */ GraphQLSegment.prototype.getMetadataBeforeCursor = function getMetadataBeforeCursor(count, cursor) { if (!this.getLength()) { return { edgeIDs: [], cursors: [] }; } var currentIndex = this._maxIndex; if (cursor) { var index = this._getIndexForCursor(cursor); if (index === undefined) { console.error('This segment does not have a cursor %s', cursor); return { edgeIDs: [], cursors: [] }; } currentIndex = index - 1; } var total = 0; var edgeIDs = []; var cursors = []; while (currentIndex >= this._minIndex && total < count) { var metadata = this._indexToMetadataMap[currentIndex]; if (!metadata.deleted) { edgeIDs.push(metadata.edgeID); cursors.push(metadata.cursor); total++; } currentIndex--; } // Reverse edges because larger index were added first return { edgeIDs: edgeIDs.reverse(), cursors: cursors.reverse() }; }; /** * @param {object} edge * @param {number} index */ GraphQLSegment.prototype._addEdgeAtIndex = function _addEdgeAtIndex(edge, index) { if (this.getLength() === 0) { this._minIndex = index; this._maxIndex = index; } else if (this._minIndex == index + 1) { this._minIndex = index; } else if (this._maxIndex == index - 1) { this._maxIndex = index; } else { console.error('Attempted to add noncontiguous index to GraphQLSegment: ' + index + ' to (' + this._minIndex + ", " + this._maxIndex + ")"); return; } var edgeID = GraphQLStoreDataHandler.getID(edge); var cursor = edge.cursor; var idIndex = this._getIndexForID(edgeID); // If the id is has an index and is not deleted if (idIndex !== undefined && this._getEdgeAtIndex(idIndex)) { console.error('Attempted to add an ID already in GraphQLSegment: %s', edgeID); return; } this._indexToMetadataMap[index] = { edgeID: edgeID, cursor: cursor, deleted: false }; this._idToIndicesMap[edgeID] = this._idToIndicesMap[edgeID] || []; this._idToIndicesMap[edgeID].unshift(index); this._count++; if (cursor) { this._cursorToIndexMap[cursor] = index; } }; /** * @param {object} edge should have cursor and a node with id */ GraphQLSegment.prototype.prependEdge = function prependEdge(edge) { this._addEdgeAtIndex(edge, this._minIndex !== null ? this._minIndex - 1 : 0); }; /** * @param {object} edge should have cursor and a node with id */ GraphQLSegment.prototype.appendEdge = function appendEdge(edge) { this._addEdgeAtIndex(edge, this._maxIndex !== null ? this._maxIndex + 1 : 0); }; /** * Mark the currently valid edge with given id to be deleted. * * @param {string} id the id of the edge to be removed */ GraphQLSegment.prototype.removeEdge = function removeEdge(id) { var index = this._getIndexForID(id); if (index === undefined) { console.error('Attempted to remove edge with ID that was never in GraphQLSegment: ' + id); return; } var data = this._indexToMetadataMap[index]; if (data.deleted) { console.error('Attempted to remove edge with ID that was already removed: ' + id); return; } data.deleted = true; this._count--; }; /** * Mark all edges with given id to be deleted. This is used by * delete mutations to ensure both the current and past edges are no longer * accessible. * * @param {string} id the id of the edge to be removed */ GraphQLSegment.prototype.removeAllEdges = function removeAllEdges(id) { var indices = this._idToIndicesMap[id]; if (!indices) { return; } for (var ii = 0; ii < indices.length; ii++) { var data = this._indexToMetadataMap[indices[ii]]; if (!data.deleted) { data.deleted = true; this._count--; } } }; /** * @param {array} edges * @param {?string} cursor */ GraphQLSegment.prototype.addEdgesAfterCursor = function addEdgesAfterCursor(edges, cursor) { // Default adding after with no cursor to -1 // So the first element in the segment is stored at index 0 var index = -1; if (cursor) { index = this._getIndexForCursor(cursor); if (index === undefined) { console.error('This segment does not have a cursor %s', cursor); return; } } while (this._maxIndex !== null && index < this._maxIndex) { var data = this._indexToMetadataMap[index + 1]; // Skip over elements that have been deleted // so we can add new edges on the end. if (data.deleted) { index++; } else { console.error('Attempted to do an overwrite to GraphQLSegment: ' + 'last index is ' + this._maxIndex + ' trying to add edges before ' + index); return; } } var startIndex = index + 1; for (var ii = 0; ii < edges.length; ii++) { var edge = edges[ii]; this._addEdgeAtIndex(edge, startIndex + ii); } }; /** * @param {array} edges - should be in increasing order of index * @param {?string} cursor */ GraphQLSegment.prototype.addEdgesBeforeCursor = function addEdgesBeforeCursor(edges, cursor) { // Default adding before with no cursor to 1 // So the first element in the segment is stored at index 0 var index = 1; if (cursor) { index = this._getIndexForCursor(cursor); if (index === undefined) { console.error('This segment does not have a cursor %s', cursor); return; } } while (this._minIndex !== null && index > this._minIndex) { var data = this._indexToMetadataMap[index - 1]; // Skip over elements that have been deleted // so we can add new edges in the front. if (data.deleted) { index--; } else { console.error('Attempted to do an overwrite to GraphQLSegment: ' + 'first index is ' + this._minIndex + ' trying to add edges after ' + index); return; } } // Edges must be added in reverse order since the // segment must be continuous at all times. var startIndex = index - 1; for (var ii = 0; ii < edges.length; ii++) { // Iterates from edges.length - 1 to 0 var edge = edges[edges.length - ii - 1]; this._addEdgeAtIndex(edge, startIndex - ii); } }; /** * This is the total length of the segment including the deleted edges. * Non-zero length guarantees value max and min indices. * DO NOT USE THIS TO DETERMINE THE TOTAL NUMBER OF EDGES; use `getCount` * instead. * @return {number} */ GraphQLSegment.prototype.getLength = function getLength() { if (this._minIndex === null && this._maxIndex === null) { return 0; } return this._maxIndex - this._minIndex + 1; }; /** * Returns the total number of non-deleted edges in the segment. * * @return {number} */ GraphQLSegment.prototype.getCount = function getCount() { return this._count; }; /** * In the event of a failed `concatSegment` operation, rollback internal * properties to their former values. * * @param {object} cursorRollbackMap * @param {object} idRollbackMap * @param {object} counters */ GraphQLSegment.prototype._rollback = function _rollback(cursorRollbackMap, idRollbackMap, counters) { _Object$assign(this._cursorToIndexMap, cursorRollbackMap); _Object$assign(this._idToIndicesMap, idRollbackMap); // no need to reset _indexToMetadataMap; resetting counters is enough this._count = counters.count; this._maxIndex = counters.maxIndex; this._minIndex = counters.minIndex; }; /** * @return {object} Captured counter state. */ GraphQLSegment.prototype._getCounterState = function _getCounterState() { return { count: this._count, maxIndex: this._maxIndex, minIndex: this._minIndex }; }; /** * Copies over content of the input segment and add to the current * segment. * @param {GraphQLSegment} segment - the segment to be copied over * @return {boolean} whether or not we successfully concatenated the segments */ GraphQLSegment.prototype.concatSegment = function concatSegment(segment) { if (!segment.getLength()) { return true; } var idRollbackMap = {}; var cursorRollbackMap = {}; var counterState = this._getCounterState(); var newEdges = segment._indexToMetadataMap; for (var ii = segment._minIndex; ii <= segment._maxIndex; ii++) { var index; if (this.getLength()) { index = this._maxIndex + 1; } else { index = 0; this._minIndex = 0; } this._maxIndex = index; var newEdge = newEdges[ii]; var idIndex = this._getIndexForID(newEdge.edgeID); if (!idRollbackMap.hasOwnProperty(newEdge.edgeID)) { if (this._idToIndicesMap[newEdge.edgeID]) { idRollbackMap[newEdge.edgeID] = this._idToIndicesMap[newEdge.edgeID].slice(); } else { idRollbackMap[newEdge.edgeID] = undefined; } } // Check for id collision. Can't have same id twice if (idIndex !== undefined) { var idEdge = this._indexToMetadataMap[idIndex]; if (idEdge.deleted && !newEdge.deleted) { // We want to map to most recent edge. Only write to the front of map // if existing edge with id is deleted or have an older deletion // time. this._idToIndicesMap[newEdge.edgeID].unshift(index); } else if (!newEdge.deleted) { console.error('Attempt to concat an ID already in GraphQLSegment: %s', newEdge.edgeID); this._rollback(cursorRollbackMap, idRollbackMap, counterState); return false; } else { // We want to keep track of past edges as well. Write these indices // to the end of the array. this._idToIndicesMap[newEdge.edgeID] = this._idToIndicesMap[newEdge.edgeID] || []; this._idToIndicesMap[newEdge.edgeID].push(index); } } else { this._idToIndicesMap[newEdge.edgeID] = this._idToIndicesMap[newEdge.edgeID] || []; this._idToIndicesMap[newEdge.edgeID].unshift(index); } var cursorIndex = this._getIndexForCursor(newEdge.cursor); // Check for cursor collision. Can't have same cursor twice if (cursorIndex !== undefined) { var cursorEdge = this._indexToMetadataMap[cursorIndex]; if (cursorEdge.deleted && !newEdge.deleted) { // We want to map to most recent edge. Only write in the cursor map if // existing edge with cursor is deleted or have and older deletion // time. cursorRollbackMap[newEdge.cursor] = this._cursorToIndexMap[newEdge.cursor]; this._cursorToIndexMap[newEdge.cursor] = index; } else if (!newEdge.deleted) { console.error('Attempt to concat a cursor already in GraphQLSegment: %s', newEdge.cursor); this._rollback(cursorRollbackMap, idRollbackMap, counterState); return false; } } else if (newEdge.cursor) { cursorRollbackMap[newEdge.cursor] = this._cursorToIndexMap[newEdge.cursor]; this._cursorToIndexMap[newEdge.cursor] = index; } if (!newEdge.deleted) { this._count++; } this._indexToMetadataMap[index] = _Object$assign({}, newEdge); } return true; }; GraphQLSegment.prototype.toJSON = function toJSON() { return [this._indexToMetadataMap, this._idToIndicesMap, this._cursorToIndexMap, this._minIndex, this._maxIndex, this._count]; }; GraphQLSegment.fromJSON = function fromJSON(descriptor) { var _descriptor = _slicedToArray(descriptor, 6); var indexToMetadataMap = _descriptor[0]; var idToIndicesMap = _descriptor[1]; var cursorToIndexMap = _descriptor[2]; var minIndex = _descriptor[3]; var maxIndex = _descriptor[4]; var count = _descriptor[5]; var segment = new GraphQLSegment(); segment._indexToMetadataMap = indexToMetadataMap; segment._idToIndicesMap = idToIndicesMap; segment._cursorToIndexMap = cursorToIndexMap; segment._minIndex = minIndex; segment._maxIndex = maxIndex; segment._count = count; return segment; }; GraphQLSegment.prototype.__debug = function __debug() { return { metadata: this._indexToMetadataMap, idToIndices: this._idToIndicesMap, cursorToIndex: this._cursorToIndexMap }; }; /** * Returns a list of all IDs that were registered for this segment. Including * edges that were deleted. */ GraphQLSegment.prototype.getEdgeIDs = function getEdgeIDs() { return _Object$keys(this._idToIndicesMap); }; return GraphQLSegment; })(); module.exports = GraphQLSegment;