1 |
|
2 |
|
3 |
|
4 |
|
5 | import { PageConfig } from '@jupyterlab/coreutils';
|
6 | import { Debouncer } from '@lumino/polling';
|
7 | import { Signal } from '@lumino/signaling';
|
8 |
|
9 |
|
10 |
|
11 | export 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 |
|
30 |
|
31 | get isDisposed() {
|
32 | return this._isDisposed;
|
33 | }
|
34 | |
35 |
|
36 |
|
37 | get recentlyOpened() {
|
38 | const recents = this._recents.opened || [];
|
39 | return recents.filter(r => r.root === this._serverRoot);
|
40 | }
|
41 | |
42 |
|
43 |
|
44 | get recentlyClosed() {
|
45 | const recents = this._recents.closed || [];
|
46 | return recents.filter(r => r.root === this._serverRoot);
|
47 | }
|
48 | |
49 |
|
50 |
|
51 | get changed() {
|
52 | return this._recentsChanged;
|
53 | }
|
54 | |
55 |
|
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 |
|
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 |
|
86 |
|
87 | addRecent(document, event) {
|
88 | const recent = {
|
89 | ...document,
|
90 | root: this._serverRoot
|
91 | };
|
92 | const recents = this._recents[event];
|
93 |
|
94 | const existingIndex = recents.findIndex(r => r.path === document.path);
|
95 | if (existingIndex >= 0) {
|
96 | recents.splice(existingIndex, 1);
|
97 | }
|
98 |
|
99 | recents.unshift(recent);
|
100 | this._setRecents(recents, event);
|
101 | this._recentsChanged.emit(undefined);
|
102 | }
|
103 | |
104 |
|
105 |
|
106 | clearRecents() {
|
107 | this._setRecents([], 'opened');
|
108 | this._setRecents([], 'closed');
|
109 | this._recentsChanged.emit(undefined);
|
110 | }
|
111 | |
112 |
|
113 |
|
114 | removeRecent(document, event) {
|
115 | this._removeRecent(document.path, [event]);
|
116 | }
|
117 | |
118 |
|
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 |
|
129 |
|
130 |
|
131 |
|
132 | updateRootDir() {
|
133 | this._serverRoot = PageConfig.getOption('serverRoot');
|
134 | }
|
135 | |
136 |
|
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 |
|
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 |
|
169 |
|
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 |
|
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 |
|
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 |
|
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 | }
|
225 | var Private;
|
226 | (function (Private) {
|
227 | |
228 |
|
229 |
|
230 | Private.stateDBKey = 'docmanager:recents';
|
231 | })(Private || (Private = {}));
|
232 |
|
\ | No newline at end of file |