UNPKG

@sanity/desk-tool

Version:

Tool for managing all sorts of content in a structured manner

286 lines (278 loc) • 12.3 kB
"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); }); }); }