db-migrate 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.js +9 -0
- package/lib/commands/generated.js +2 -1
- package/lib/commands/work.js +33 -0
- package/lib/dml.js +463 -101
- package/lib/driver/index.js +2 -0
- package/lib/jobs.js +302 -0
- package/lib/state.js +6 -0
- package/lib/walker.js +118 -5
- package/lib/work.js +274 -0
- package/package.json +2 -2
package/lib/driver/index.js
CHANGED
|
@@ -166,6 +166,8 @@ exports.connect = function (config, intern, callback) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
trackSql(db);
|
|
169
|
+
// the dialect of parameters in all, $1 instead of ? for pg
|
|
170
|
+
db._dbmDriver = typeof config.driver === 'string' ? config.driver : null;
|
|
169
171
|
callback(null, db);
|
|
170
172
|
}
|
|
171
173
|
);
|
package/lib/jobs.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const log = require('db-migrate-shared').log;
|
|
5
|
+
const State = require('./state');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Background migrations, dml migrations with _meta.background, are not run
|
|
9
|
+
* by up, but registered as jobs. Workers started by executeWork take the
|
|
10
|
+
* jobs one by one and run them. A migration counts as run once its job is
|
|
11
|
+
* done, until then it is running.
|
|
12
|
+
*
|
|
13
|
+
* All jobs live in one row of the state table, changed by compare and swap
|
|
14
|
+
* only, so any number of workers can work on them at once:
|
|
15
|
+
*
|
|
16
|
+
* s: queued, running or failed
|
|
17
|
+
* blocking: later jobs wait until this one is done
|
|
18
|
+
* ID: the worker running it, n: renewed by every write of the worker, a
|
|
19
|
+
* job whose n does not change for the timeout is taken over
|
|
20
|
+
* step, learned, done, rb: the progress of the run, like for migrations run
|
|
21
|
+
* in the foreground, see State
|
|
22
|
+
* h: hash of the migration file when it started
|
|
23
|
+
* err: why it failed
|
|
24
|
+
*
|
|
25
|
+
* Migrations take precedence over jobs. Whoever holds the migration lock
|
|
26
|
+
* pauses the jobs before migrating: no job is taken anymore, the running
|
|
27
|
+
* ones stop after their current batch, and are continued once the lock is
|
|
28
|
+
* released. The pause is stored with the jobs, so taking a job and pausing
|
|
29
|
+
* never miss each other. It holds as long as its holder holds the lock.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const JOBS = '__dbmigrate_jobs__';
|
|
33
|
+
|
|
34
|
+
const nonce = () => crypto.randomBytes(8).toString('hex');
|
|
35
|
+
|
|
36
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
37
|
+
|
|
38
|
+
const WAIT = { wait: true };
|
|
39
|
+
|
|
40
|
+
const read = async (pdriver, internals) => {
|
|
41
|
+
const row = await pdriver._getKV(internals.migrationState, JOBS);
|
|
42
|
+
const state = row ? JSON.parse(row.value) : { jobs: {} };
|
|
43
|
+
return { row, state, jobs: state.jobs };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const list = async (pdriver, internals) => (await read(pdriver, internals)).jobs;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Changes the jobs by mutate(jobs, state), retried until no other worker
|
|
50
|
+
* wrote in between. mutate returns false to leave them as they are.
|
|
51
|
+
*/
|
|
52
|
+
const change = async (pdriver, internals, mutate) => {
|
|
53
|
+
for (let attempt = 0; ; attempt++) {
|
|
54
|
+
const { row, state, jobs } = await read(pdriver, internals);
|
|
55
|
+
|
|
56
|
+
if (mutate(jobs, state) === false) {
|
|
57
|
+
return jobs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
state.n = nonce();
|
|
61
|
+
const value = JSON.stringify(state);
|
|
62
|
+
|
|
63
|
+
if (!row) {
|
|
64
|
+
try {
|
|
65
|
+
await pdriver._insertKV(internals.migrationState, JOBS, value);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// inserted by another worker first, try again with theirs
|
|
68
|
+
log.verbose('[jobs] insert failed', err.message);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
await pdriver._updateKVC(
|
|
72
|
+
internals.migrationState,
|
|
73
|
+
JOBS,
|
|
74
|
+
value,
|
|
75
|
+
'value',
|
|
76
|
+
row.value
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const after = await pdriver._getKV(internals.migrationState, JOBS);
|
|
81
|
+
if (after && after.value === value) {
|
|
82
|
+
return jobs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (attempt > 50) {
|
|
86
|
+
throw new Error('[jobs] could not change the jobs, too much contention');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await delay(Math.floor(Math.random() * 20 * Math.min(attempt + 1, 10)));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Registers a migration as job, or a failed one again. A job which failed
|
|
95
|
+
* with irreversible steps keeps its progress, so it continues after them.
|
|
96
|
+
*/
|
|
97
|
+
const register = async (pdriver, internals, file) => {
|
|
98
|
+
const blocking = (file.get()._meta || {}).blocking === true;
|
|
99
|
+
|
|
100
|
+
await change(pdriver, internals, jobs => {
|
|
101
|
+
const job = jobs[file.name];
|
|
102
|
+
|
|
103
|
+
if (job && job.s !== 'failed') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
jobs[file.name] = Object.assign(
|
|
108
|
+
{ step: 0, learned: 0, done: 0, rb: 0 },
|
|
109
|
+
job,
|
|
110
|
+
{ s: 'queued', blocking, ID: 0, n: nonce(), err: undefined }
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const mig = await pdriver._getKV(internals.migrationState, file.name);
|
|
115
|
+
if (!mig) {
|
|
116
|
+
await pdriver._insertKV(internals.migrationState, file.name, '{}');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log.info(`[jobs] ${file.name} runs in the background`);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether the jobs are paused by a migration. A pause holds as long as its
|
|
124
|
+
* holder holds the migration lock and keeps it alive, the lock row did not
|
|
125
|
+
* stay the same for the lock timeout, measured on our clock in seen.
|
|
126
|
+
*
|
|
127
|
+
* Resolves to the pause, or null.
|
|
128
|
+
*/
|
|
129
|
+
const paused = async (pdriver, internals, seen = {}) => {
|
|
130
|
+
const { state } = await read(pdriver, internals);
|
|
131
|
+
|
|
132
|
+
if (!state.pause) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lock = await State.readLock(pdriver, internals);
|
|
137
|
+
if (!lock || State.holder(lock) !== state.pause.ID) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const last = seen.lock;
|
|
143
|
+
if (!last || last.value !== lock.value || String(last.run_on) !== String(lock.run_on)) {
|
|
144
|
+
seen.lock = { value: lock.value, run_on: lock.run_on, since: now };
|
|
145
|
+
} else if (now - last.since >= State.lockOptions(internals).timeout) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return state.pause;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Takes the first job to run, in the order of their names. A running job is
|
|
154
|
+
* taken over if it did not change for timeout ms, measured on our clock in
|
|
155
|
+
* seen. Jobs after a blocking job wait until it is done.
|
|
156
|
+
*
|
|
157
|
+
* Resolves to the name of the job taken, null if there is none, or WAIT
|
|
158
|
+
* while the jobs are paused.
|
|
159
|
+
*/
|
|
160
|
+
const claim = async (pdriver, internals, id, seen, timeout) => {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const valid = await paused(pdriver, internals, seen);
|
|
163
|
+
let taken = null;
|
|
164
|
+
let wait = false;
|
|
165
|
+
|
|
166
|
+
await change(pdriver, internals, (jobs, state) => {
|
|
167
|
+
taken = null;
|
|
168
|
+
// a pause found invalid before is ignored, one set in between holds
|
|
169
|
+
wait = !!state.pause && (!!valid || state.pause.n !== seen.invalid);
|
|
170
|
+
if (state.pause && !valid) {
|
|
171
|
+
seen.invalid = seen.invalid || state.pause.n;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (wait) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const name of Object.keys(jobs).sort()) {
|
|
179
|
+
const job = jobs[name];
|
|
180
|
+
let free = job.s === 'queued';
|
|
181
|
+
|
|
182
|
+
if (job.s === 'running' && job.ID !== id) {
|
|
183
|
+
const last = seen[name];
|
|
184
|
+
|
|
185
|
+
if (!last || last.n !== job.n) {
|
|
186
|
+
seen[name] = { n: job.n, since: now };
|
|
187
|
+
} else if (now - last.since >= timeout) {
|
|
188
|
+
log.warn(
|
|
189
|
+
`[jobs] taking over ${name}, its worker did not respond for ` +
|
|
190
|
+
`${timeout} ms`
|
|
191
|
+
);
|
|
192
|
+
free = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (free) {
|
|
197
|
+
Object.assign(job, { s: 'running', ID: id, n: nonce() });
|
|
198
|
+
taken = name;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (job.blocking) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (wait) {
|
|
211
|
+
return WAIT;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return taken;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const isRunning = (job, id) => job.s === 'running' && job.ID !== id;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Pauses the jobs for the migrations of the lock holder id, and waits until
|
|
221
|
+
* the running jobs stopped. A job whose worker does not respond for the lock
|
|
222
|
+
* timeout is not waited for, it is continued by another worker later.
|
|
223
|
+
*/
|
|
224
|
+
const pause = async (pdriver, internals, id) => {
|
|
225
|
+
const { timeout, interval } = State.lockOptions(internals);
|
|
226
|
+
const seen = {};
|
|
227
|
+
|
|
228
|
+
await change(pdriver, internals, (jobs, state) => {
|
|
229
|
+
state.pause = { ID: id, n: nonce() };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
for (;;) {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const jobs = await list(pdriver, internals);
|
|
235
|
+
const running = Object.keys(jobs).filter(name => {
|
|
236
|
+
const job = jobs[name];
|
|
237
|
+
|
|
238
|
+
if (!isRunning(job, id)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const last = seen[name];
|
|
243
|
+
if (!last || last.n !== job.n) {
|
|
244
|
+
seen[name] = { n: job.n, since: now };
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return now - last.since < timeout;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!running.length) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
log.info(
|
|
256
|
+
`[jobs] waiting for ${running.length} background job(s) to pause: ` +
|
|
257
|
+
running.join(', ')
|
|
258
|
+
);
|
|
259
|
+
// they stop after their current batch, usually soon
|
|
260
|
+
await delay(Math.min(interval, 200));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Forgets a job, once its migration is reverted.
|
|
266
|
+
*/
|
|
267
|
+
const remove = (pdriver, internals, name) =>
|
|
268
|
+
change(pdriver, internals, jobs => {
|
|
269
|
+
if (!jobs[name]) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
delete jobs[name];
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const resume = (pdriver, internals, id) =>
|
|
277
|
+
change(pdriver, internals, (jobs, state) => {
|
|
278
|
+
if (!state.pause || state.pause.ID !== id) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
delete state.pause;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Changes a job we run. If another worker took it over in between, lost is
|
|
287
|
+
* called and nothing is changed.
|
|
288
|
+
*/
|
|
289
|
+
const update = (pdriver, internals, name, id, mutate, lost = () => {}) =>
|
|
290
|
+
change(pdriver, internals, jobs => {
|
|
291
|
+
const job = jobs[name];
|
|
292
|
+
|
|
293
|
+
if (!job || job.ID !== id) {
|
|
294
|
+
lost();
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
mutate(job, jobs);
|
|
299
|
+
job.n = nonce();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
module.exports = { JOBS, WAIT, list, register, claim, update, paused, pause, resume, remove };
|
package/lib/state.js
CHANGED
|
@@ -155,6 +155,12 @@ const claim = (row, driver, internals) => {
|
|
|
155
155
|
module.exports = {
|
|
156
156
|
createConcurrently,
|
|
157
157
|
|
|
158
|
+
// the id of this process in the lock, the holder of a lock row, see Jobs
|
|
159
|
+
ID,
|
|
160
|
+
holder,
|
|
161
|
+
lockOptions,
|
|
162
|
+
readLock: read,
|
|
163
|
+
|
|
158
164
|
// for tests, simulates a process which died while holding the lock
|
|
159
165
|
__stopHeartbeat: stopHeartbeat,
|
|
160
166
|
|
package/lib/walker.js
CHANGED
|
@@ -5,6 +5,8 @@ const State = require('./state');
|
|
|
5
5
|
const log = require('db-migrate-shared').log;
|
|
6
6
|
const Promise = require('bluebird');
|
|
7
7
|
const File = require('./file.js');
|
|
8
|
+
const Dml = require('./dml');
|
|
9
|
+
const Jobs = require('./jobs');
|
|
8
10
|
|
|
9
11
|
const INTERFACES = {
|
|
10
12
|
migration: require('./interface/migratorInterface.js'),
|
|
@@ -125,14 +127,110 @@ Walker.prototype = {
|
|
|
125
127
|
return run(toRun);
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
let paused = false;
|
|
128
131
|
try {
|
|
132
|
+
// migrations take precedence over the jobs of background migrations
|
|
133
|
+
await Jobs.pause(this._pdriver, this.internals, State.ID);
|
|
134
|
+
paused = true;
|
|
129
135
|
await State.reloadSchema(this._pdriver, this.internals);
|
|
130
136
|
return await run(await determine());
|
|
131
137
|
} finally {
|
|
138
|
+
if (paused) {
|
|
139
|
+
await Jobs.resume(this._pdriver, this.internals, State.ID).catch(err =>
|
|
140
|
+
log.warn(`[jobs] could not resume the jobs: ${err.message}`)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
await State.release(this._pdriver, this.internals);
|
|
133
145
|
}
|
|
134
146
|
},
|
|
135
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Migrations running in the background are left out, until their job is
|
|
150
|
+
* done. Failed ones are run, which registers them again.
|
|
151
|
+
*/
|
|
152
|
+
_withoutJobs: async function (toRun) {
|
|
153
|
+
if (this.prefix !== 'migration' || !this._pdriver || typeof this._pdriver._getKV !== 'function') {
|
|
154
|
+
return toRun;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const jobs = await Jobs.list(this._pdriver, this.internals);
|
|
158
|
+
|
|
159
|
+
return toRun.filter(file => {
|
|
160
|
+
const job = jobs[file.name];
|
|
161
|
+
|
|
162
|
+
if (job && job.s !== 'failed') {
|
|
163
|
+
log.info(`${this.title}${file.name} is running in the background`);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The migrations of the jobs, newest first.
|
|
173
|
+
*/
|
|
174
|
+
_jobFiles: async function () {
|
|
175
|
+
if (this.prefix !== 'migration' || !this._pdriver || typeof this._pdriver._getKV !== 'function') {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const jobs = await Jobs.list(this._pdriver, this.internals);
|
|
180
|
+
const names = Object.keys(jobs);
|
|
181
|
+
if (!names.length) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const files = await File.loadFromFileystem(this.directory, this.prefix, this.internals);
|
|
186
|
+
return names
|
|
187
|
+
.sort()
|
|
188
|
+
.reverse()
|
|
189
|
+
.map(name => {
|
|
190
|
+
const file = files.find(f => f.name === name);
|
|
191
|
+
if (!file) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`The migration of the background job "${name}" is missing, it ` +
|
|
194
|
+
'can not be reverted'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
file.job = true;
|
|
199
|
+
return file;
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reverts what the job of a background migration did so far, it was
|
|
205
|
+
* paused by taking the lock, and forgets the job.
|
|
206
|
+
*/
|
|
207
|
+
_revertJob: async function (file) {
|
|
208
|
+
log.info(`${this.title}reverting the background job of ${file.name}`);
|
|
209
|
+
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
210
|
+
|
|
211
|
+
if (!this.internals.dryRun) {
|
|
212
|
+
await Dml.down(this, this.driver, file);
|
|
213
|
+
await Jobs.remove(this._pdriver, this.internals, file.name);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
_registerJob: async function (file) {
|
|
218
|
+
Dml.checkType(file);
|
|
219
|
+
|
|
220
|
+
if (this.internals.dryRun) {
|
|
221
|
+
log.info(`${this.title}${file.name} would run in the background`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!State.supportsLock(this._pdriver)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'Background migrations need a driver supporting the migration lock'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Jobs.register(this._pdriver, this.internals, file);
|
|
232
|
+
},
|
|
233
|
+
|
|
136
234
|
writeMigrationRecord: function (migration, callback) {
|
|
137
235
|
const onComplete = err => {
|
|
138
236
|
if (err) {
|
|
@@ -201,9 +299,11 @@ Walker.prototype = {
|
|
|
201
299
|
this._driver,
|
|
202
300
|
this.internals
|
|
203
301
|
)
|
|
204
|
-
])
|
|
205
|
-
|
|
206
|
-
|
|
302
|
+
])
|
|
303
|
+
.spread((allFiles, completedFiles) =>
|
|
304
|
+
dbmUtil.filterUp(allFiles, completedFiles, partialName, count)
|
|
305
|
+
)
|
|
306
|
+
.then(toRun => this._withoutJobs(toRun));
|
|
207
307
|
|
|
208
308
|
if (this.internals.check) {
|
|
209
309
|
return determine()
|
|
@@ -219,6 +319,10 @@ Walker.prototype = {
|
|
|
219
319
|
|
|
220
320
|
return Promise.each(toRun, file => {
|
|
221
321
|
log.verbose(this.title + 'preparing to run up:', file.name);
|
|
322
|
+
if (Dml.isBackground(file)) {
|
|
323
|
+
return this._registerJob(file).catch(failedIn(file));
|
|
324
|
+
}
|
|
325
|
+
|
|
222
326
|
const _meta = file.get()._meta || {};
|
|
223
327
|
const version = _meta.version || 1;
|
|
224
328
|
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
@@ -243,8 +347,13 @@ Walker.prototype = {
|
|
|
243
347
|
this.prefix,
|
|
244
348
|
this._driver,
|
|
245
349
|
this.internals
|
|
246
|
-
).then(completedFiles =>
|
|
247
|
-
dbmUtil.filterDown(
|
|
350
|
+
).then(async completedFiles =>
|
|
351
|
+
dbmUtil.filterDown(
|
|
352
|
+
// jobs of background migrations are the latest, still running
|
|
353
|
+
(await this._jobFiles()).concat(completedFiles),
|
|
354
|
+
partialName,
|
|
355
|
+
count
|
|
356
|
+
)
|
|
248
357
|
);
|
|
249
358
|
|
|
250
359
|
if (this.internals.check) {
|
|
@@ -261,6 +370,10 @@ Walker.prototype = {
|
|
|
261
370
|
|
|
262
371
|
return Promise.each(toRun, file => {
|
|
263
372
|
log.verbose(this.title + 'preparing to run down:', file.name);
|
|
373
|
+
if (file.job) {
|
|
374
|
+
return this._revertJob(file).catch(failedIn(file));
|
|
375
|
+
}
|
|
376
|
+
|
|
264
377
|
const _meta = file.get()._meta || {};
|
|
265
378
|
const version = _meta.version || 1;
|
|
266
379
|
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|