@sanity/desk-tool
Version:
Tool for managing all sorts of content in a structured manner
286 lines (278 loc) • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Controller = void 0;
exports.createObservableController = createObservableController;
var _rxjs = require("rxjs");
var _internal = require("@sanity/base/_internal");
var _getJsonStream = require("./getJsonStream");
var _Aligner = require("./Aligner");
var _Reconstruction = require("./Reconstruction");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
var TRANSLOG_ENTRY_LIMIT = 50;
/**
* The controller is responsible for fetching information
* about a document and maintaining a Timeline.
*/
class Controller {
constructor(options) {
_defineProperty(this, "timeline", void 0);
_defineProperty(this, "client", void 0);
_defineProperty(this, "handler", void 0);
_defineProperty(this, "version", 0);
/**
* The selection state represents the different states of the current selection:
* - inactive: No selection is active.
* - rev: A selection is active for a single revision.
* - range: A selection is active for a range and we have all the data needed to render it.
* - loading: A selection is active, but we don't have the entries yet.
* - invalid: The selection picked is invalid.
*/
_defineProperty(this, "selectionState", 'inactive');
_defineProperty(this, "_aligner", void 0);
_defineProperty(this, "_fetchMore", false);
_defineProperty(this, "_fetchAtLeast", 0);
_defineProperty(this, "_isRunning", false);
_defineProperty(this, "_didErr", false);
_defineProperty(this, "_since", null);
_defineProperty(this, "_sinceTime", null);
_defineProperty(this, "_rev", null);
_defineProperty(this, "_revTime", null);
_defineProperty(this, "_reconstruction", void 0);
this.timeline = options.timeline;
this.client = options.client;
this.handler = options.handler;
this._aligner = new _Aligner.Aligner(this.timeline);
this.markChange();
}
clearRange() {
this.setRange(null, null);
}
setRange(since, rev) {
if (rev !== this._rev) this.setRevTime(rev);
if (since !== this._since) this.setSinceTime(since);
var _fetchAtLeast = 10;
if (this._sinceTime === 'loading' || this._revTime === 'loading' || !this._aligner.isAligned) {
this.selectionState = 'loading';
} else if (this._sinceTime === 'invalid' || this._revTime === 'invalid') {
this.selectionState = 'invalid';
} else if (this._sinceTime) {
this.selectionState = 'range';
var targetRev = this._revTime || this.timeline.lastChunk();
if (this._sinceTime.index > targetRev.index) {
this._revTime = 'invalid';
this.selectionState = 'invalid';
} else {
this.setReconstruction(this._sinceTime, targetRev);
}
} else if (this._revTime) {
this.selectionState = 'rev';
this.setReconstruction(null, this._revTime);
} else {
this.selectionState = 'inactive';
_fetchAtLeast = 0;
}
this._fetchAtLeast = _fetchAtLeast;
this.start();
}
setLoadMore(state) {
this._fetchMore = state;
this.start();
}
get sinceTime() {
return this._sinceTime && typeof this._sinceTime === 'object' ? this._sinceTime : null;
}
get revTime() {
return this._revTime && typeof this._revTime === 'object' ? this._revTime : null;
}
get realRevChunk() {
return this.revTime || this.timeline.lastChunk();
}
/** Returns true when there's an older revision we want to render. */
onOlderRevision() {
return Boolean(this._rev) && (this.selectionState === 'range' || this.selectionState === 'rev');
}
/** Returns true when the changes panel should be active. */
changesPanelActive() {
return Boolean(this._since) && this.selectionState === 'range';
}
findRangeForNewRev(rev) {
var revTimeId = this.timeline.isLatestChunk(rev) ? null : this.timeline.createTimeId(rev);
if (!this._since) {
return [null, revTimeId];
}
var sinceChunk = this.sinceTime;
if (sinceChunk && sinceChunk.index < rev.index) {
return [this._since, revTimeId];
}
return ['@lastPublished', revTimeId];
}
findRangeForNewSince(since) {
var revChunk = this.revTime;
// If the the `since` timestamp is earlier than the `rev`, then we can
// accept it. Otherwise we'll move the current revision to the current draft.
if (revChunk && since.index < revChunk.index) {
return [this.timeline.createTimeId(since), this._rev];
}
return [this.timeline.createTimeId(since), null];
}
setRevTime(rev) {
this._rev = rev;
this._revTime = rev ? this.timeline.parseTimeId(rev) : null;
if (this._since === '@lastPublished') {
// Make sure we invalidate it since this depends on the _rev.
this._since = null;
this._sinceTime = null;
}
}
setSinceTime(since) {
if (since === '@lastPublished') {
if (typeof this._revTime === 'string') {
this._sinceTime = this._revTime;
} else {
this._sinceTime = this.timeline.findLastPublishedBefore(this._revTime);
}
} else {
this._sinceTime = since ? this.timeline.parseTimeId(since) : null;
}
this._since = since;
}
sinceAttributes() {
return this._sinceTime && this._reconstruction ? this._reconstruction.startAttributes() : null;
}
displayed() {
return this._revTime && this._reconstruction ? this._reconstruction.endAttributes() : null;
}
setReconstruction(since, rev) {
if (this._reconstruction && this._reconstruction.same(since, rev)) return;
this._reconstruction = new _Reconstruction.Reconstruction(this.timeline, this._aligner.currentDocument, since, rev);
}
currentDiff() {
return this._reconstruction ? this._reconstruction.diff() : null;
}
currentObjectDiff() {
var diff = this.currentDiff();
if (diff) {
if (diff.type === 'null') return null;
if (diff.type !== 'object') throw new Error("ObjectDiff expected, got ".concat(diff.type));
}
return diff;
}
handleRemoteMutation(ev) {
this._aligner.appendRemoteSnapshotEvent(ev);
this.markChange();
// Make sure we fetch history as soon as possible.
if (this._aligner.acceptsHistory) this.start();
}
start() {
if (this._didErr) return;
if (!this._isRunning) {
this._isRunning = true;
this.tick().then(() => {
this._isRunning = false;
});
}
}
tick() {
var _this = this;
return _asyncToGenerator(function* () {
var shouldFetchMore = _this._aligner.acceptsHistory && !_this.timeline.reachedEarliestEntry && (_this.selectionState === 'loading' || _this._fetchMore || _this.timeline.chunkCount <= _this._fetchAtLeast);
if (!shouldFetchMore) {
return;
}
try {
yield _this.fetchMoreTransactions();
} catch (err) {
_this._didErr = true;
_this.handler(err, _this);
return;
}
yield _this.tick();
})();
}
fetchMoreTransactions() {
var _this2 = this;
return _asyncToGenerator(function* () {
var publishedId = _this2.timeline.publishedId;
var draftId = _this2.timeline.draftId;
var dataset = _this2.client.config().dataset;
var limit = TRANSLOG_ENTRY_LIMIT;
var queryParams = "tag=sanity.studio.desk.history&effectFormat=mendoza&excludeContent=true&excludeMutations=true&includeIdentifiedDocumentsOnly=true&reverse=true&limit=".concat(limit);
var tid = _this2._aligner.earliestTransactionId;
if (tid) {
queryParams += "&toTransaction=".concat(tid);
}
var url = "/data/history/".concat(dataset, "/transactions/").concat(publishedId, ",").concat(draftId, "?").concat(queryParams);
var _this2$client$config = _this2.client.config(),
token = _this2$client$config.token;
var stream = yield (0, _getJsonStream.getJsonStream)(_this2.client.getUrl(url), token || undefined);
var reader = stream.getReader();
var count = 0;
for (;;) {
// eslint-disable-next-line no-await-in-loop
var result = yield reader.read();
if (result.done) break;
if ('error' in result.value) {
throw new Error(result.value.error.description || result.value.error.type);
}
count++;
if (result.value.id === tid) {
// toTransaction is inclusive so we must ignore it when we fetch the next page
continue;
}
// For some reason, the aligner is now interested in a different set of entries.
// This can happen if a new snapshot comes in as we're streaming the translog.
// In this case it's safe to abort, and the run-loop will re-schedule it correctly.
if (_this2._aligner.earliestTransactionId !== tid || !_this2._aligner.acceptsHistory) {
return;
}
_this2._aligner.prependHistoryEvent(result.value);
tid = _this2._aligner.earliestTransactionId;
}
// Same consistency checking here:
if (_this2._aligner.earliestTransactionId !== tid || !_this2._aligner.acceptsHistory) {
return;
}
if (count < limit) {
_this2._aligner.didReachEarliestEntry();
}
_this2.markChange();
})();
}
markChange() {
this.timeline.updateChunks();
this.setRevTime(this._rev);
this.setSinceTime(this._rev);
this.version++;
this.handler(null, this);
}
}
exports.Controller = Controller;
function createObservableController(options) {
return new _rxjs.Observable(observer => {
var controller = new Controller(_objectSpread(_objectSpread({}, options), {}, {
handler: (err, innerController) => {
if (err) {
observer.error(err);
} else {
observer.next({
historyController: innerController
});
}
}
}));
return (0, _internal.remoteSnapshots)({
publishedId: options.documentId,
draftId: "drafts.".concat(options.documentId)
}, options.documentType).subscribe(ev => {
controller.handleRemoteMutation(ev);
});
});
}