UNPKG

6.81 kBJavaScriptView Raw
1/*
2 * Copyright (c) Jupyter Development Team.
3 * Distributed under the terms of the Modified BSD License.
4 */
5import { PageConfig } from '@jupyterlab/coreutils';
6import { Debouncer } from '@lumino/polling';
7import { Signal } from '@lumino/signaling';
8/**
9 * Manager for recently opened and closed documents.
10 */
11export class RecentsManager {
12 constructor(options) {
13 this._recentsChanged = new Signal(this);
14 this._recents = {
15 opened: [],
16 closed: []
17 };
18 this._isDisposed = false;
19 this._maxRecentsLength = 10;
20 this._saveDebouncer = new Debouncer(this._save.bind(this), 500);
21 this._stateDB = options.stateDB;
22 this._contentsManager = options.contents;
23 this.updateRootDir();
24 this._loadRecents().catch(r => {
25 console.error(`Failed to load recent list from state:\n${r}`);
26 });
27 }
28 /**
29 * Whether the manager is disposed or not.
30 */
31 get isDisposed() {
32 return this._isDisposed;
33 }
34 /**
35 * List of recently opened items
36 */
37 get recentlyOpened() {
38 const recents = this._recents.opened || [];
39 return recents.filter(r => r.root === this._serverRoot);
40 }
41 /**
42 * List of recently opened items
43 */
44 get recentlyClosed() {
45 const recents = this._recents.closed || [];
46 return recents.filter(r => r.root === this._serverRoot);
47 }
48 /**
49 * Signal emitted when the recent list changes.
50 */
51 get changed() {
52 return this._recentsChanged;
53 }
54 /**
55 * Maximal number of recent items to list.
56 */
57 get maximalRecentsLength() {
58 return this._maxRecentsLength;
59 }
60 set maximalRecentsLength(value) {
61 this._maxRecentsLength = Math.round(Math.max(1, value));
62 let changed = false;
63 for (const type of ['opened', 'closed']) {
64 if (this._recents[type].length > this._maxRecentsLength) {
65 this._recents[type].length = this._maxRecentsLength;
66 changed = true;
67 }
68 }
69 if (changed) {
70 this._recentsChanged.emit(undefined);
71 }
72 }
73 /**
74 * Dispose recent manager resources
75 */
76 dispose() {
77 if (this.isDisposed) {
78 return;
79 }
80 this._isDisposed = true;
81 Signal.clearData(this);
82 this._saveDebouncer.dispose();
83 }
84 /**
85 * Add a new path to the recent list.
86 */
87 addRecent(document, event) {
88 const recent = {
89 ...document,
90 root: this._serverRoot
91 };
92 const recents = this._recents[event];
93 // Check if it's already present; if so remove it
94 const existingIndex = recents.findIndex(r => r.path === document.path);
95 if (existingIndex >= 0) {
96 recents.splice(existingIndex, 1);
97 }
98 // Add to the front of the list
99 recents.unshift(recent);
100 this._setRecents(recents, event);
101 this._recentsChanged.emit(undefined);
102 }
103 /**
104 * Clear the recents list
105 */
106 clearRecents() {
107 this._setRecents([], 'opened');
108 this._setRecents([], 'closed');
109 this._recentsChanged.emit(undefined);
110 }
111 /**
112 * Remove the document from recents list.
113 */
114 removeRecent(document, event) {
115 this._removeRecent(document.path, [event]);
116 }
117 /**
118 * Check if the recent item is valid, remove if it from both lists if it is not.
119 */
120 async validate(recent) {
121 const valid = await this._isValid(recent);
122 if (!valid) {
123 this._removeRecent(recent.path);
124 }
125 return valid;
126 }
127 /**
128 * Set server root dir.
129 *
130 * Note: protected to allow unit-testing.
131 */
132 updateRootDir() {
133 this._serverRoot = PageConfig.getOption('serverRoot');
134 }
135 /**
136 * Remove a path from both lists (opened and closed).
137 */
138 _removeRecent(path, lists = ['opened', 'closed']) {
139 let changed = false;
140 for (const type of lists) {
141 const recents = this._recents[type];
142 const newRecents = recents.filter(r => path !== r.path);
143 if (recents.length !== newRecents.length) {
144 this._setRecents(newRecents, type);
145 changed = true;
146 }
147 }
148 if (changed) {
149 this._recentsChanged.emit(undefined);
150 }
151 }
152 /**
153 * Check if the path of a given recent document exists.
154 */
155 async _isValid(recent) {
156 var _a;
157 try {
158 await this._contentsManager.get(recent.path, { content: false });
159 }
160 catch (e) {
161 if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) === 404) {
162 return false;
163 }
164 }
165 return true;
166 }
167 /**
168 * Set the recent list
169 * @param recents The new recent list
170 */
171 _setRecents(recents, type) {
172 this._recents[type] = recents
173 .slice(0, this.maximalRecentsLength)
174 .sort((a, b) => {
175 if (a.root === b.root) {
176 return 0;
177 }
178 else {
179 return a.root !== this._serverRoot ? 1 : -1;
180 }
181 });
182 this._saveDebouncer.invoke().catch(console.warn);
183 }
184 /**
185 * Load the recent items from the state.
186 */
187 async _loadRecents() {
188 const recents = (await this._stateDB.fetch(Private.stateDBKey)) || {
189 opened: [],
190 closed: []
191 };
192 const allRecents = [...recents.opened, ...recents.closed];
193 const invalidPaths = new Set(await this._getInvalidPaths(allRecents));
194 for (const type of ['opened', 'closed']) {
195 this._setRecents(recents[type].filter(r => !invalidPaths.has(r.path)), type);
196 }
197 this._recentsChanged.emit(undefined);
198 }
199 /**
200 * Get the list of invalid path in recents.
201 */
202 async _getInvalidPaths(recents) {
203 const invalidPathsOrNulls = await Promise.all(recents.map(async (r) => {
204 if (await this._isValid(r)) {
205 return null;
206 }
207 else {
208 return r.path;
209 }
210 }));
211 return invalidPathsOrNulls.filter(x => typeof x === 'string');
212 }
213 /**
214 * Save the recent items to the state.
215 */
216 async _save() {
217 try {
218 await this._stateDB.save(Private.stateDBKey, this._recents);
219 }
220 catch (e) {
221 console.log('Saving recents failed', e);
222 }
223 }
224}
225var Private;
226(function (Private) {
227 /**
228 * Key reserved in the state database.
229 */
230 Private.stateDBKey = 'docmanager:recents';
231})(Private || (Private = {}));
232//# sourceMappingURL=recents.js.map
\No newline at end of file