mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
* Shutdown Doc worker when it is not considered as available in Redis * Use isAffirmative for GRIST_MANAGED_WORKERS * Upgrade Sinon for the tests * Run Smoke test with pages in English * Add logic in /status endpoint
This commit is contained in:
parent
dd83b7f678
commit
4a9b6fea9d
@ -24,6 +24,9 @@ const CHECKSUM_TTL_MSEC = 24 * 60 * 60 * 1000; // 24 hours
|
|||||||
// How long do permits stored in redis last, in milliseconds.
|
// How long do permits stored in redis last, in milliseconds.
|
||||||
const PERMIT_TTL_MSEC = 1 * 60 * 1000; // 1 minute
|
const PERMIT_TTL_MSEC = 1 * 60 * 1000; // 1 minute
|
||||||
|
|
||||||
|
// Default doc worker group.
|
||||||
|
const DEFAULT_GROUP = 'default';
|
||||||
|
|
||||||
class DummyDocWorkerMap implements IDocWorkerMap {
|
class DummyDocWorkerMap implements IDocWorkerMap {
|
||||||
private _worker?: DocWorkerInfo;
|
private _worker?: DocWorkerInfo;
|
||||||
private _available: boolean = false;
|
private _available: boolean = false;
|
||||||
@ -62,6 +65,10 @@ class DummyDocWorkerMap implements IDocWorkerMap {
|
|||||||
this._available = available;
|
this._available = available;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isWorkerRegistered(workerInfo: DocWorkerInfo): Promise<boolean> {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
public async releaseAssignment(workerId: string, docId: string): Promise<void> {
|
public async releaseAssignment(workerId: string, docId: string): Promise<void> {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
@ -241,7 +248,7 @@ export class DocWorkerMap implements IDocWorkerMap {
|
|||||||
try {
|
try {
|
||||||
// Drop out of available set first.
|
// Drop out of available set first.
|
||||||
await this._client.sremAsync('workers-available', workerId);
|
await this._client.sremAsync('workers-available', workerId);
|
||||||
const group = await this._client.getAsync(`worker-${workerId}-group`) || 'default';
|
const group = await this._client.getAsync(`worker-${workerId}-group`) || DEFAULT_GROUP;
|
||||||
await this._client.sremAsync(`workers-available-${group}`, workerId);
|
await this._client.sremAsync(`workers-available-${group}`, workerId);
|
||||||
// At this point, this worker should no longer be receiving new doc assignments, though
|
// At this point, this worker should no longer be receiving new doc assignments, though
|
||||||
// clients may still be directed to the worker.
|
// clients may still be directed to the worker.
|
||||||
@ -290,7 +297,7 @@ export class DocWorkerMap implements IDocWorkerMap {
|
|||||||
|
|
||||||
public async setWorkerAvailability(workerId: string, available: boolean): Promise<void> {
|
public async setWorkerAvailability(workerId: string, available: boolean): Promise<void> {
|
||||||
log.info(`DocWorkerMap.setWorkerAvailability ${workerId} ${available}`);
|
log.info(`DocWorkerMap.setWorkerAvailability ${workerId} ${available}`);
|
||||||
const group = await this._client.getAsync(`worker-${workerId}-group`) || 'default';
|
const group = await this._client.getAsync(`worker-${workerId}-group`) || DEFAULT_GROUP;
|
||||||
if (available) {
|
if (available) {
|
||||||
const docWorker = await this._client.hgetallAsync(`worker-${workerId}`) as DocWorkerInfo|null;
|
const docWorker = await this._client.hgetallAsync(`worker-${workerId}`) as DocWorkerInfo|null;
|
||||||
if (!docWorker) { throw new Error('no doc worker contact info available'); }
|
if (!docWorker) { throw new Error('no doc worker contact info available'); }
|
||||||
@ -306,6 +313,11 @@ export class DocWorkerMap implements IDocWorkerMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isWorkerRegistered(workerInfo: DocWorkerInfo): Promise<boolean> {
|
||||||
|
const group = workerInfo.group || DEFAULT_GROUP;
|
||||||
|
return Boolean(await this._client.sismemberAsync(`workers-available-${group}`, workerInfo.id));
|
||||||
|
}
|
||||||
|
|
||||||
public async releaseAssignment(workerId: string, docId: string): Promise<void> {
|
public async releaseAssignment(workerId: string, docId: string): Promise<void> {
|
||||||
const op = this._client.multi();
|
const op = this._client.multi();
|
||||||
op.del(`doc-${docId}`);
|
op.del(`doc-${docId}`);
|
||||||
@ -352,7 +364,7 @@ export class DocWorkerMap implements IDocWorkerMap {
|
|||||||
if (docId === 'import') {
|
if (docId === 'import') {
|
||||||
const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT);
|
const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT);
|
||||||
try {
|
try {
|
||||||
const _workerId = await this._client.srandmemberAsync(`workers-available-default`);
|
const _workerId = await this._client.srandmemberAsync(`workers-available-${DEFAULT_GROUP}`);
|
||||||
if (!_workerId) { throw new Error('no doc worker available'); }
|
if (!_workerId) { throw new Error('no doc worker available'); }
|
||||||
const docWorker = await this._client.hgetallAsync(`worker-${_workerId}`) as DocWorkerInfo|null;
|
const docWorker = await this._client.hgetallAsync(`worker-${_workerId}`) as DocWorkerInfo|null;
|
||||||
if (!docWorker) { throw new Error('no doc worker contact info available'); }
|
if (!docWorker) { throw new Error('no doc worker contact info available'); }
|
||||||
@ -383,7 +395,7 @@ export class DocWorkerMap implements IDocWorkerMap {
|
|||||||
|
|
||||||
if (!workerId) {
|
if (!workerId) {
|
||||||
// Check if document has a preferred worker group set.
|
// Check if document has a preferred worker group set.
|
||||||
const group = await this._client.getAsync(`doc-${docId}-group`) || 'default';
|
const group = await this._client.getAsync(`doc-${docId}-group`) || DEFAULT_GROUP;
|
||||||
|
|
||||||
// Let's start off by assigning documents to available workers randomly.
|
// Let's start off by assigning documents to available workers randomly.
|
||||||
// TODO: use a smarter algorithm.
|
// TODO: use a smarter algorithm.
|
||||||
|
@ -57,6 +57,8 @@ export interface IDocWorkerMap extends IPermitStores, IElectionStore, IChecksumS
|
|||||||
// release existing assignments.
|
// release existing assignments.
|
||||||
setWorkerAvailability(workerId: string, available: boolean): Promise<void>;
|
setWorkerAvailability(workerId: string, available: boolean): Promise<void>;
|
||||||
|
|
||||||
|
isWorkerRegistered(workerInfo: DocWorkerInfo): Promise<boolean>;
|
||||||
|
|
||||||
// Releases doc from worker, freeing it to be assigned elsewhere.
|
// Releases doc from worker, freeing it to be assigned elsewhere.
|
||||||
// Assignments should only be released for workers that are now unavailable.
|
// Assignments should only be released for workers that are now unavailable.
|
||||||
releaseAssignment(workerId: string, docId: string): Promise<void>;
|
releaseAssignment(workerId: string, docId: string): Promise<void>;
|
||||||
|
@ -445,7 +445,8 @@ export class FlexServer implements GristServer {
|
|||||||
// /status/hooks allows the tests to wait for them to be ready.
|
// /status/hooks allows the tests to wait for them to be ready.
|
||||||
// If db=1 query parameter is included, status will include the status of DB connection.
|
// If db=1 query parameter is included, status will include the status of DB connection.
|
||||||
// If redis=1 query parameter is included, status will include the status of the Redis connection.
|
// If redis=1 query parameter is included, status will include the status of the Redis connection.
|
||||||
// If ready=1 query parameter is included, status will include whether the server is fully ready.
|
// If docWorkerRegistered=1 query parameter is included, status will include the status of the
|
||||||
|
// doc worker registration in Redis.
|
||||||
this.app.get('/status(/hooks)?', async (req, res) => {
|
this.app.get('/status(/hooks)?', async (req, res) => {
|
||||||
const checks = new Map<string, Promise<boolean>|boolean>();
|
const checks = new Map<string, Promise<boolean>|boolean>();
|
||||||
const timeout = optIntegerParam(req.query.timeout, 'timeout') || 10_000;
|
const timeout = optIntegerParam(req.query.timeout, 'timeout') || 10_000;
|
||||||
@ -467,6 +468,15 @@ export class FlexServer implements GristServer {
|
|||||||
if (isParameterOn(req.query.redis)) {
|
if (isParameterOn(req.query.redis)) {
|
||||||
checks.set('redis', asyncCheck(this._docWorkerMap.getRedisClient()?.pingAsync()));
|
checks.set('redis', asyncCheck(this._docWorkerMap.getRedisClient()?.pingAsync()));
|
||||||
}
|
}
|
||||||
|
if (isParameterOn(req.query.docWorkerRegistered) && this.worker) {
|
||||||
|
// Only check whether the doc worker is registered if we have a worker.
|
||||||
|
// The Redis client may not be connected, but in this case this has to
|
||||||
|
// be checked with the 'redis' parameter (the user may want to avoid
|
||||||
|
// removing workers when connection is unstable).
|
||||||
|
if (this._docWorkerMap.getRedisClient()?.connected) {
|
||||||
|
checks.set('docWorkerRegistered', asyncCheck(this._docWorkerMap.isWorkerRegistered(this.worker)));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isParameterOn(req.query.ready)) {
|
if (isParameterOn(req.query.ready)) {
|
||||||
checks.set('ready', this._isReady);
|
checks.set('ready', this._isReady);
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
fallback: {
|
fallback: {
|
||||||
'path': require.resolve("path-browserify"),
|
'path': require.resolve("path-browserify"),
|
||||||
|
'process': require.resolve("process/browser"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@ -79,7 +80,7 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
// Some modules assume presence of Buffer and process.
|
// Some modules assume presence of Buffer and process.
|
||||||
new ProvidePlugin({
|
new ProvidePlugin({
|
||||||
process: 'process/browser',
|
process: 'process',
|
||||||
Buffer: ['buffer', 'Buffer']
|
Buffer: ['buffer', 'Buffer']
|
||||||
}),
|
}),
|
||||||
// To strip all locales except “en”
|
// To strip all locales except “en”
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"test:client": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'",
|
"test:client": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'",
|
||||||
"test:common": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'",
|
"test:common": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'",
|
||||||
"test:server": "TEST_CLEAN_DATABASE=true TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} -g \"${GREP_TESTS}\" -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'",
|
"test:server": "TEST_CLEAN_DATABASE=true TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} -g \"${GREP_TESTS}\" -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'",
|
||||||
"test:smoke": "mocha _build/test/nbrowser/Smoke.js",
|
"test:smoke": "LANGUAGE=en_US mocha _build/test/nbrowser/Smoke.js",
|
||||||
"test:docker": "./test/test_under_docker.sh",
|
"test:docker": "./test/test_under_docker.sh",
|
||||||
"test:python": "sandbox_venv3/bin/python sandbox/grist/runtests.py ${GREP_TESTS:+discover -p \"test*${GREP_TESTS}*.py\"}",
|
"test:python": "sandbox_venv3/bin/python sandbox/grist/runtests.py ${GREP_TESTS:+discover -p \"test*${GREP_TESTS}*.py\"}",
|
||||||
"cli": "NODE_PATH=_build:_build/stubs:_build/ext node _build/app/server/companion.js",
|
"cli": "NODE_PATH=_build:_build/stubs:_build/ext node _build/app/server/companion.js",
|
||||||
@ -76,7 +76,7 @@
|
|||||||
"@types/redlock": "3.0.2",
|
"@types/redlock": "3.0.2",
|
||||||
"@types/saml2-js": "2.0.1",
|
"@types/saml2-js": "2.0.1",
|
||||||
"@types/selenium-webdriver": "4.1.15",
|
"@types/selenium-webdriver": "4.1.15",
|
||||||
"@types/sinon": "5.0.5",
|
"@types/sinon": "17.0.3",
|
||||||
"@types/sqlite3": "3.1.6",
|
"@types/sqlite3": "3.1.6",
|
||||||
"@types/swagger-ui": "3.52.4",
|
"@types/swagger-ui": "3.52.4",
|
||||||
"@types/tmp": "0.0.33",
|
"@types/tmp": "0.0.33",
|
||||||
@ -100,7 +100,7 @@
|
|||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
"otplib": "12.0.1",
|
"otplib": "12.0.1",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"sinon": "7.1.1",
|
"sinon": "17.0.1",
|
||||||
"source-map-loader": "^0.2.4",
|
"source-map-loader": "^0.2.4",
|
||||||
"tmp-promise": "1.0.5",
|
"tmp-promise": "1.0.5",
|
||||||
"ts-interface-builder": "0.3.2",
|
"ts-interface-builder": "0.3.2",
|
||||||
|
1
stubs/app/server/declarations.d.ts
vendored
1
stubs/app/server/declarations.d.ts
vendored
@ -28,6 +28,7 @@ declare module "redis" {
|
|||||||
function createClient(url?: string): RedisClient;
|
function createClient(url?: string): RedisClient;
|
||||||
|
|
||||||
class RedisClient {
|
class RedisClient {
|
||||||
|
public readonly connected: boolean;
|
||||||
public eval(args: any[], callback?: (err: Error | null, res: any) => void): any;
|
public eval(args: any[], callback?: (err: Error | null, res: any) => void): any;
|
||||||
|
|
||||||
public subscribe(channel: string): void;
|
public subscribe(channel: string): void;
|
||||||
|
@ -46,7 +46,7 @@ describe('SafeBrowser', function() {
|
|||||||
|
|
||||||
browserProcesses = [];
|
browserProcesses = [];
|
||||||
sandbox.stub(SafeBrowser, 'createWorker').callsFake(createProcess);
|
sandbox.stub(SafeBrowser, 'createWorker').callsFake(createProcess);
|
||||||
sandbox.stub(SafeBrowser, 'createView').callsFake(createProcess);
|
sandbox.stub(SafeBrowser, 'createView').callsFake(createProcess as any);
|
||||||
sandbox.stub(PluginInstance.prototype, 'getRenderTarget').returns(noop);
|
sandbox.stub(PluginInstance.prototype, 'getRenderTarget').returns(noop);
|
||||||
disposeSpy = sandbox.spy(Disposable.prototype, 'dispose');
|
disposeSpy = sandbox.spy(Disposable.prototype, 'dispose');
|
||||||
});
|
});
|
||||||
|
@ -153,9 +153,8 @@ describe('dispose', function() {
|
|||||||
assert.equal(baz.dispose.callCount, 1);
|
assert.equal(baz.dispose.callCount, 1);
|
||||||
assert(baz.dispose.calledBefore(bar.dispose));
|
assert(baz.dispose.calledBefore(bar.dispose));
|
||||||
|
|
||||||
const name = consoleErrors[0][1]; // may be Foo, or minified.
|
const name = consoleErrors[0][1];
|
||||||
assert(name === 'Foo' || name === 'o'); // this may not be reliable,
|
assert(name === Foo.name);
|
||||||
// just what I happen to see.
|
|
||||||
assert.deepEqual(consoleErrors[0], ['Error constructing %s:', name, 'Error: test-error1']);
|
assert.deepEqual(consoleErrors[0], ['Error constructing %s:', name, 'Error: test-error1']);
|
||||||
assert.deepEqual(consoleErrors[1], ['Error constructing %s:', name, 'Error: test-error2']);
|
assert.deepEqual(consoleErrors[1], ['Error constructing %s:', name, 'Error: test-error2']);
|
||||||
assert.deepEqual(consoleErrors[2], ['Error constructing %s:', name, 'Error: test-error3']);
|
assert.deepEqual(consoleErrors[2], ['Error constructing %s:', name, 'Error: test-error3']);
|
||||||
|
56
test/gen-server/lib/DocWorkerMap.ts
Normal file
56
test/gen-server/lib/DocWorkerMap.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Test for DocWorkerMap.ts
|
||||||
|
|
||||||
|
import { DocWorkerMap } from 'app/gen-server/lib/DocWorkerMap';
|
||||||
|
import { DocWorkerInfo } from 'app/server/lib/DocWorkerMap';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
describe('DocWorkerMap', () => {
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isWorkerRegistered', () => {
|
||||||
|
const baseWorkerInfo: DocWorkerInfo = {
|
||||||
|
id: 'workerId',
|
||||||
|
internalUrl: 'internalUrl',
|
||||||
|
publicUrl: 'publicUrl',
|
||||||
|
group: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
itMsg: 'should check if worker is registered',
|
||||||
|
sisMemberAsyncResolves: 1,
|
||||||
|
expectedResult: true,
|
||||||
|
expectedKey: 'workers-available-default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itMsg: 'should check if worker is registered in a certain group',
|
||||||
|
sisMemberAsyncResolves: 1,
|
||||||
|
group: 'dummygroup',
|
||||||
|
expectedResult: true,
|
||||||
|
expectedKey: 'workers-available-dummygroup'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itMsg: 'should return false if worker is not registered',
|
||||||
|
sisMemberAsyncResolves: 0,
|
||||||
|
expectedResult: false,
|
||||||
|
expectedKey: 'workers-available-default'
|
||||||
|
}
|
||||||
|
].forEach(ctx => {
|
||||||
|
it(ctx.itMsg, async () => {
|
||||||
|
const sismemberAsyncStub = sinon.stub().resolves(ctx.sisMemberAsyncResolves);
|
||||||
|
const stubDocWorkerMap = {
|
||||||
|
_client: { sismemberAsync: sismemberAsyncStub }
|
||||||
|
};
|
||||||
|
const result = await DocWorkerMap.prototype.isWorkerRegistered.call(
|
||||||
|
stubDocWorkerMap, {...baseWorkerInfo, group: ctx.group }
|
||||||
|
);
|
||||||
|
expect(result).to.equal(ctx.expectedResult);
|
||||||
|
expect(sismemberAsyncStub.calledOnceWith(ctx.expectedKey, baseWorkerInfo.id)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -402,20 +402,19 @@ describe('Comm', function() {
|
|||||||
|
|
||||||
// Intercept the call to _onClose to know when it occurs, since we are trying to hit a
|
// Intercept the call to _onClose to know when it occurs, since we are trying to hit a
|
||||||
// situation where 'close' and 'failedSend' events happen in either order.
|
// situation where 'close' and 'failedSend' events happen in either order.
|
||||||
const stubOnClose = sandbox.stub(Client.prototype as any, '_onClose')
|
const stubOnClose: any = sandbox.stub(Client.prototype as any, '_onClose')
|
||||||
.callsFake(async function(this: Client) {
|
.callsFake(function(this: Client) {
|
||||||
if (!options.closeHappensFirst) { await delay(10); }
|
|
||||||
eventsSeen.push('close');
|
eventsSeen.push('close');
|
||||||
return (stubOnClose as any).wrappedMethod.apply(this, arguments);
|
return stubOnClose.wrappedMethod.apply(this, arguments);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Intercept calls to client.sendMessage(), to know when it fails, and possibly to delay the
|
// Intercept calls to client.sendMessage(), to know when it fails, and possibly to delay the
|
||||||
// failures to hit a particular order in which 'close' and 'failedSend' events are seen by
|
// failures to hit a particular order in which 'close' and 'failedSend' events are seen by
|
||||||
// Client.ts. This is the only reliable way I found to reproduce this order of events.
|
// Client.ts. This is the only reliable way I found to reproduce this order of events.
|
||||||
const stubSendToWebsocket = sandbox.stub(Client.prototype as any, '_sendToWebsocket')
|
const stubSendToWebsocket: any = sandbox.stub(Client.prototype as any, '_sendToWebsocket')
|
||||||
.callsFake(async function(this: Client) {
|
.callsFake(async function(this: Client) {
|
||||||
try {
|
try {
|
||||||
return await (stubSendToWebsocket as any).wrappedMethod.apply(this, arguments);
|
return await stubSendToWebsocket.wrappedMethod.apply(this, arguments);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (options.closeHappensFirst) { await delay(100); }
|
if (options.closeHappensFirst) { await delay(100); }
|
||||||
eventsSeen.push('failedSend');
|
eventsSeen.push('failedSend');
|
||||||
|
@ -48,7 +48,7 @@ describe("MinIOExternalStorage", function () {
|
|||||||
|
|
||||||
s3.listObjects.returns(fakeStream);
|
s3.listObjects.returns(fakeStream);
|
||||||
|
|
||||||
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3);
|
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3 as any);
|
||||||
const result = await extStorage.versions(key);
|
const result = await extStorage.versions(key);
|
||||||
|
|
||||||
assert.deepEqual(result, []);
|
assert.deepEqual(result, []);
|
||||||
@ -74,7 +74,7 @@ describe("MinIOExternalStorage", function () {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
s3.listObjects.returns(fakeStream);
|
s3.listObjects.returns(fakeStream);
|
||||||
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3);
|
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3 as any);
|
||||||
// when
|
// when
|
||||||
const result = await extStorage.versions(key);
|
const result = await extStorage.versions(key);
|
||||||
// then
|
// then
|
||||||
@ -107,7 +107,7 @@ describe("MinIOExternalStorage", function () {
|
|||||||
let {fakeStream} = makeFakeStream(objectsFromS3);
|
let {fakeStream} = makeFakeStream(objectsFromS3);
|
||||||
|
|
||||||
s3.listObjects.returns(fakeStream);
|
s3.listObjects.returns(fakeStream);
|
||||||
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3);
|
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3 as any);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await extStorage.versions(key);
|
const result = await extStorage.versions(key);
|
||||||
@ -142,10 +142,10 @@ describe("MinIOExternalStorage", function () {
|
|||||||
const fakeStream = new stream.Readable({objectMode: true});
|
const fakeStream = new stream.Readable({objectMode: true});
|
||||||
const error = new Error("dummy-error");
|
const error = new Error("dummy-error");
|
||||||
sandbox.stub(fakeStream, "_read")
|
sandbox.stub(fakeStream, "_read")
|
||||||
.returns(fakeStream)
|
.returns(fakeStream as any)
|
||||||
.callsFake(() => fakeStream.emit('error', error));
|
.callsFake(() => fakeStream.emit('error', error));
|
||||||
s3.listObjects.returns(fakeStream);
|
s3.listObjects.returns(fakeStream);
|
||||||
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3);
|
const extStorage = new MinIOExternalStorage(dummyBucket, dummyOptions, 42, s3 as any);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = extStorage.versions(key);
|
const result = extStorage.versions(key);
|
||||||
|
159
yarn.lock
159
yarn.lock
@ -510,39 +510,40 @@
|
|||||||
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz"
|
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz"
|
||||||
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
||||||
|
|
||||||
"@sinonjs/commons@^1", "@sinonjs/commons@^1.2.0", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^2.0.0":
|
||||||
version "1.8.3"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz"
|
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
|
||||||
integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==
|
integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
|
||||||
dependencies:
|
dependencies:
|
||||||
type-detect "4.0.8"
|
type-detect "4.0.8"
|
||||||
|
|
||||||
"@sinonjs/formatio@^3.0.0", "@sinonjs/formatio@^3.2.1":
|
"@sinonjs/commons@^3.0.0":
|
||||||
version "3.2.2"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz"
|
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd"
|
||||||
integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
|
integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^1"
|
type-detect "4.0.8"
|
||||||
"@sinonjs/samsam" "^3.1.0"
|
|
||||||
|
|
||||||
"@sinonjs/samsam@^2.1.2":
|
"@sinonjs/fake-timers@^11.2.2":
|
||||||
version "2.1.3"
|
version "11.2.2"
|
||||||
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz"
|
resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz#50063cc3574f4a27bd8453180a04171c85cc9699"
|
||||||
integrity sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==
|
integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==
|
||||||
|
|
||||||
"@sinonjs/samsam@^3.1.0":
|
|
||||||
version "3.3.3"
|
|
||||||
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz"
|
|
||||||
integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^1.3.0"
|
"@sinonjs/commons" "^3.0.0"
|
||||||
array-from "^2.1.1"
|
|
||||||
lodash "^4.17.15"
|
|
||||||
|
|
||||||
"@sinonjs/text-encoding@^0.7.1":
|
"@sinonjs/samsam@^8.0.0":
|
||||||
version "0.7.1"
|
version "8.0.0"
|
||||||
resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz"
|
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz#0d488c91efb3fa1442e26abea81759dfc8b5ac60"
|
||||||
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
|
integrity sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^2.0.0"
|
||||||
|
lodash.get "^4.4.2"
|
||||||
|
type-detect "^4.0.8"
|
||||||
|
|
||||||
|
"@sinonjs/text-encoding@^0.7.2":
|
||||||
|
version "0.7.2"
|
||||||
|
resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
|
||||||
|
integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
|
||||||
|
|
||||||
"@socket.io/component-emitter@~3.1.0":
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
@ -951,10 +952,17 @@
|
|||||||
"@types/mime" "*"
|
"@types/mime" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/sinon@5.0.5":
|
"@types/sinon@17.0.3":
|
||||||
version "5.0.5"
|
version "17.0.3"
|
||||||
resolved "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.5.tgz"
|
resolved "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa"
|
||||||
integrity sha512-Wnuv66VhvAD2LEJfZkq8jowXGxe+gjVibeLCYcVBp7QLdw0BFx2sRkKzoiiDkYEPGg5VyqO805Rcj0stVjQwCQ==
|
integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==
|
||||||
|
dependencies:
|
||||||
|
"@types/sinonjs__fake-timers" "*"
|
||||||
|
|
||||||
|
"@types/sinonjs__fake-timers@*":
|
||||||
|
version "8.1.5"
|
||||||
|
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2"
|
||||||
|
integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==
|
||||||
|
|
||||||
"@types/sizzle@*":
|
"@types/sizzle@*":
|
||||||
version "2.3.2"
|
version "2.3.2"
|
||||||
@ -1581,11 +1589,6 @@ array-flatten@1.1.1:
|
|||||||
resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
|
||||||
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
|
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
|
||||||
|
|
||||||
array-from@^2.1.1:
|
|
||||||
version "2.1.1"
|
|
||||||
resolved "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz"
|
|
||||||
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
|
|
||||||
|
|
||||||
array-map@~0.0.0:
|
array-map@~0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz"
|
resolved "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz"
|
||||||
@ -2948,10 +2951,10 @@ diff@5.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
||||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
||||||
|
|
||||||
diff@^3.5.0:
|
diff@^5.1.0:
|
||||||
version "3.5.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz"
|
resolved "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
|
||||||
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
|
integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
|
||||||
|
|
||||||
diffie-hellman@^5.0.0:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
@ -4833,11 +4836,6 @@ is-yarn-global@^0.3.0:
|
|||||||
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz"
|
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz"
|
||||||
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
|
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
|
||||||
|
|
||||||
isarray@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
|
||||||
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
|
||||||
|
|
||||||
isarray@~1.0.0:
|
isarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
@ -5037,10 +5035,10 @@ jszip@^3.10.1, jszip@^3.5.0:
|
|||||||
readable-stream "~2.3.6"
|
readable-stream "~2.3.6"
|
||||||
setimmediate "^1.0.5"
|
setimmediate "^1.0.5"
|
||||||
|
|
||||||
just-extend@^4.0.2:
|
just-extend@^6.2.0:
|
||||||
version "4.2.1"
|
version "6.2.0"
|
||||||
resolved "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz"
|
resolved "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947"
|
||||||
integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
|
integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==
|
||||||
|
|
||||||
jwa@^1.4.1:
|
jwa@^1.4.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
@ -5307,18 +5305,6 @@ log-symbols@4.1.0:
|
|||||||
chalk "^4.1.0"
|
chalk "^4.1.0"
|
||||||
is-unicode-supported "^0.1.0"
|
is-unicode-supported "^0.1.0"
|
||||||
|
|
||||||
lolex@^3.0.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz"
|
|
||||||
integrity sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==
|
|
||||||
|
|
||||||
lolex@^5.0.1:
|
|
||||||
version "5.1.2"
|
|
||||||
resolved "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz"
|
|
||||||
integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
|
|
||||||
dependencies:
|
|
||||||
"@sinonjs/commons" "^1.7.0"
|
|
||||||
|
|
||||||
loupe@^2.3.1:
|
loupe@^2.3.1:
|
||||||
version "2.3.6"
|
version "2.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53"
|
resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53"
|
||||||
@ -5796,16 +5782,16 @@ nice-napi@^1.0.2:
|
|||||||
node-addon-api "^3.0.0"
|
node-addon-api "^3.0.0"
|
||||||
node-gyp-build "^4.2.2"
|
node-gyp-build "^4.2.2"
|
||||||
|
|
||||||
nise@^1.4.6:
|
nise@^5.1.5:
|
||||||
version "1.5.3"
|
version "5.1.9"
|
||||||
resolved "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz"
|
resolved "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz#0cb73b5e4499d738231a473cd89bd8afbb618139"
|
||||||
integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==
|
integrity sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/formatio" "^3.2.1"
|
"@sinonjs/commons" "^3.0.0"
|
||||||
"@sinonjs/text-encoding" "^0.7.1"
|
"@sinonjs/fake-timers" "^11.2.2"
|
||||||
just-extend "^4.0.2"
|
"@sinonjs/text-encoding" "^0.7.2"
|
||||||
lolex "^5.0.1"
|
just-extend "^6.2.0"
|
||||||
path-to-regexp "^1.7.0"
|
path-to-regexp "^6.2.1"
|
||||||
|
|
||||||
node-abort-controller@3.0.1:
|
node-abort-controller@3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
@ -6237,12 +6223,10 @@ path-to-regexp@0.1.7:
|
|||||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||||
|
|
||||||
path-to-regexp@^1.7.0:
|
path-to-regexp@^6.2.1:
|
||||||
version "1.8.0"
|
version "6.2.1"
|
||||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz"
|
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
|
||||||
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==
|
||||||
dependencies:
|
|
||||||
isarray "0.0.1"
|
|
||||||
|
|
||||||
path-type@^4.0.0:
|
path-type@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
@ -7209,20 +7193,17 @@ simple-concat@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz"
|
||||||
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
|
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
|
||||||
|
|
||||||
sinon@7.1.1:
|
sinon@17.0.1:
|
||||||
version "7.1.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.npmjs.org/sinon/-/sinon-7.1.1.tgz"
|
resolved "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz#26b8ef719261bf8df43f925924cccc96748e407a"
|
||||||
integrity sha512-iYagtjLVt1vN3zZY7D8oH7dkjNJEjLjyuzy8daX5+3bbQl8gaohrheB9VfH1O3L6LKuue5WTJvFluHiuZ9y3nQ==
|
integrity sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^1.2.0"
|
"@sinonjs/commons" "^3.0.0"
|
||||||
"@sinonjs/formatio" "^3.0.0"
|
"@sinonjs/fake-timers" "^11.2.2"
|
||||||
"@sinonjs/samsam" "^2.1.2"
|
"@sinonjs/samsam" "^8.0.0"
|
||||||
diff "^3.5.0"
|
diff "^5.1.0"
|
||||||
lodash.get "^4.4.2"
|
nise "^5.1.5"
|
||||||
lolex "^3.0.0"
|
supports-color "^7.2.0"
|
||||||
nise "^1.4.6"
|
|
||||||
supports-color "^5.5.0"
|
|
||||||
type-detect "^4.0.8"
|
|
||||||
|
|
||||||
slash@^3.0.0:
|
slash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
@ -7502,7 +7483,7 @@ supports-color@^5.3.0, supports-color@^5.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^3.0.0"
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
supports-color@^7.1.0:
|
supports-color@^7.1.0, supports-color@^7.2.0:
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||||
|
Loading…
Reference in New Issue
Block a user