async-mutex#Mutex TypeScript Examples

The following examples show how to use async-mutex#Mutex. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts    From webminidisc with GNU General Public License v2.0 7 votes vote down vote up
export function asyncMutex(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // This is meant to be used only with classes having a "mutex" instance property
    const oldValue = descriptor.value;
    descriptor.value = async function(...args: any) {
        const mutex = (this as any).mutex as Mutex;
        const release = await mutex.acquire();
        try {
            return await oldValue.apply(this, args);
        } finally {
            release();
        }
    };
    return descriptor;
}
Example #2
Source File: leopard.ts    From leopard with Apache License 2.0 6 votes vote down vote up
private constructor(handleWasm: LeopardWasmOutput) {
    Leopard._sampleRate = handleWasm.sampleRate;
    Leopard._version = handleWasm.version;

    this._pvLeopardDelete = handleWasm.pvLeopardDelete;
    this._pvLeopardProcess = handleWasm.pvLeopardProcess;
    this._pvStatusToString = handleWasm.pvStatusToString;

    this._wasmMemory = handleWasm.memory;
    this._pvFree = handleWasm.pvFree;
    this._objectAddress = handleWasm.objectAddress;
    this._alignedAlloc = handleWasm.aligned_alloc;
    this._transcriptionAddressAddress = handleWasm.transcriptionAddressAddress;

    this._memoryBuffer = new Int16Array(handleWasm.memory.buffer);
    this._memoryBufferUint8 = new Uint8Array(handleWasm.memory.buffer);
    this._memoryBufferView = new DataView(handleWasm.memory.buffer);
    this._processMutex = new Mutex();
  }
Example #3
Source File: index.ts    From zilswap-sdk with MIT License 6 votes vote down vote up
/**
   * Creates the Zilswap SDK object. {@linkcode initalize} needs to be called after
   * the object is created to begin watching the blockchain's state.
   *
   * @param network the Network to use, either `TestNet` or `MainNet`.
   * @param walletProviderOrKey a Provider with Wallet or private key string to be used for signing txns.
   * @param options a set of Options that will be used for all txns.
   */
  constructor(readonly network: Network, walletProviderOrKey?: WalletProvider | string, options?: Options) {
    this.rpcEndpoint = options?.rpcEndpoint || APIS[network]
    if (typeof walletProviderOrKey === 'string') {
      this.zilliqa = new Zilliqa(this.rpcEndpoint)
      this.zilliqa.wallet.addByPrivateKey(walletProviderOrKey)
    } else if (walletProviderOrKey) {
      this.zilliqa = new Zilliqa(this.rpcEndpoint, walletProviderOrKey.provider)
      this.walletProvider = walletProviderOrKey
    } else {
      this.zilliqa = new Zilliqa(this.rpcEndpoint)
    }

    this.contractAddress = CONTRACTS[network]
    this.contract = (this.walletProvider || this.zilliqa).contracts.at(this.contractAddress)
    this.contractHash = fromBech32Address(this.contractAddress).toLowerCase()
    this.tokens = {}
    this.zilos = {}
    this._txParams.version = CHAIN_VERSIONS[network]

    if (options) {
      if (options.deadlineBuffer && options.deadlineBuffer > 0) this.deadlineBuffer = options.deadlineBuffer
      if (options.gasPrice && options.gasPrice > 0) this._txParams.gasPrice = toPositiveQa(options.gasPrice, units.Units.Li)
      if (options.gasLimit && options.gasLimit > 0) this._txParams.gasLimit = Long.fromNumber(options.gasLimit)
    }

    this.observerMutex = new Mutex()
  }
Example #4
Source File: task.ts    From command-bot with Apache License 2.0 5 votes vote down vote up
tasksRepositoryLockMutex = new Mutex()
Example #5
Source File: makeBaselets.ts    From edge-currency-plugins with MIT License 5 votes vote down vote up
makeBaselets = async (
  config: MakeBaseletsConfig
): Promise<Baselets> => {
  /* Tables */
  const addressBases: AddressBaselets = {
    addressByScriptPubkey: await createOrOpenHashBase(
      config.disklet,
      addressByScriptPubkeyOptions
    ),
    scriptPubkeyByPath: await createOrOpenCountBase(
      config.disklet,
      scriptPubkeyByPathOptions
    ),
    lastUsedByFormatPath: await createOrOpenHashBase(
      config.disklet,
      lastUsedByFormatPathOptions
    )
  }
  const txBases: TransactionBaselets = {
    txById: await createOrOpenHashBase(config.disklet, txByIdOptions),
    txIdsByBlockHeight: await createOrOpenRangeBase(
      config.disklet,
      txIdsByBlockHeightOptions
    ),
    txIdsByDate: await createOrOpenRangeBase(config.disklet, txIdsByDateOptions)
  }
  const utxoBases: UtxoBaselets = {
    utxoById: await createOrOpenHashBase(config.disklet, utxoByIdOptions),
    utxoIdsByScriptPubkey: await createOrOpenHashBase(
      config.disklet,
      utxoIdsByScriptPubkeyOptions
    )
  }

  const addressMutex = new Mutex()
  const txMutex = new Mutex()
  const utxoMutex = new Mutex()

  return {
    async address(fn): Promise<ReturnType<typeof fn>> {
      return await addressMutex.runExclusive(async () => await fn(addressBases))
    },

    async tx(fn): Promise<ReturnType<typeof fn>> {
      return await txMutex.runExclusive(async () => await fn(txBases))
    },

    async utxo(fn): Promise<ReturnType<typeof fn>> {
      return await utxoMutex.runExclusive(async () => await fn(utxoBases))
    },

    all: {
      ...addressBases,
      ...txBases,
      ...utxoBases
    }
  }
}
Example #6
Source File: github.ts    From command-bot with Apache License 2.0 5 votes vote down vote up
requestMutex = new Mutex()
Example #7
Source File: capacitor-storage-preferences.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
private readonly mutex = new Mutex();
Example #8
Source File: media-store.service.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
private readonly mutex = new Mutex();
Example #9
Source File: capacitor-filesystem-table.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
private readonly mutex = new Mutex();
Example #10
Source File: capacitor-filesystem-table.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
private static readonly initializationMutex = new Mutex();
Example #11
Source File: index.ts    From integration-services with Apache License 2.0 5 votes vote down vote up
acquire(key: string) {
		if (!this.locks.has(key)) {
			this.locks.set(key, new Mutex());
		}
		return this.locks.get(key).acquire();
	}
Example #12
Source File: currency.ts    From cloud-pricing-api with Apache License 2.0 5 votes vote down vote up
mutex = new Mutex()
Example #13
Source File: nonce-tracker.ts    From hoprnet with GNU General Public License v3.0 5 votes vote down vote up
private lockMap: Record<string, Mutex>
Example #14
Source File: nonce-tracker.ts    From hoprnet with GNU General Public License v3.0 5 votes vote down vote up
private _lookupMutex(lockId: string): Mutex {
    let mutex = this.lockMap[lockId]
    if (!mutex) {
      mutex = new Mutex()
      this.lockMap[lockId] = mutex
    }
    return mutex
  }
Example #15
Source File: daemon.ts    From edge-impulse-cli with Apache License 2.0 5 votes vote down vote up
private _snapshotMutex = new Mutex();
Example #16
Source File: netmd.ts    From webminidisc with GNU General Public License v2.0 5 votes vote down vote up
public mutex = new Mutex();
Example #17
Source File: netmd-mock.ts    From webminidisc with GNU General Public License v2.0 5 votes vote down vote up
public mutex = new Mutex();
Example #18
Source File: scheduler.ts    From backend with MIT License 5 votes vote down vote up
activeConnectionMutex = new Mutex()
Example #19
Source File: manualExecution.ts    From backend with MIT License 5 votes vote down vote up
executeJob = async (job) => {
    const activeConnectionMutex = new Mutex();
    const release = await activeConnectionMutex.acquire();
    const jobConnection = await createConnection();
    release();

    switch (job) {
        case 'initialInterestConfirmationRequests': {
            initialInterestConfirmationRequests(jobConnection.manager);
            break;
        }
        case 'screeningReminderJob': {
            screeningReminderJob(jobConnection.manager);
            break;
        }
        case 'courseReminderJob': {
            courseReminderJob(jobConnection.manager);
            break;
        }
        case 'feedbackRequestJob': {
            feedbackRequestJob(jobConnection.manager);
            break;
        }
        case 'matchFollowUpJob': {
            matchFollowUpJob(jobConnection.manager);
            break;
        }
        case 'jufoVerificationInfo': {
            jufoVerificationInfo(jobConnection.manager);
            break;
        }
        case 'projectMatchMaking': {
            projectMatchMaking(jobConnection.manager);
            break;
        }
        case 'tutoringMatchMaking': {
            tutoringMatchMaking(jobConnection.manager);
            break;
        }
        case 'interestConfirmationRequestReminders': {
            interestConfirmationRequestReminders(jobConnection.manager);
            break;
        }
        case 'Notification': {
            Notification.checkReminders();
            break;
        }
        case 'deactivateMissingCoc': {
            deactivateMissingCoc();
            break;
        }
        default: {
            throw new Error(`Did not find job ${job}`);
        }
    }
}
Example #20
Source File: index.ts    From zilswap-sdk with MIT License 5 votes vote down vote up
private observerMutex: Mutex
Example #21
Source File: revision_cache.ts    From skynet-js with MIT License 5 votes vote down vote up
mutex: Mutex;
Example #22
Source File: revision_cache.ts    From skynet-js with MIT License 5 votes vote down vote up
/**
   * Creates a `CachedRevisionNumber`.
   */
  constructor() {
    this.mutex = new Mutex();
    this.revision = BigInt(-1);
  }
Example #23
Source File: revision_cache.ts    From skynet-js with MIT License 5 votes vote down vote up
private mutex: Mutex;
Example #24
Source File: revision_cache.ts    From skynet-js with MIT License 5 votes vote down vote up
/**
   * Creates the `RevisionNumberCache`.
   */
  constructor() {
    this.mutex = new Mutex();
    this.cache = {};
  }
Example #25
Source File: FtxService.ts    From sakeperp-arbitrageur with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
private readonly txMutex = new Mutex()
Example #26
Source File: Arbitrageur.ts    From sakeperp-arbitrageur with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
private readonly nonceMutex = new Mutex()
Example #27
Source File: leopard.ts    From leopard with Apache License 2.0 5 votes vote down vote up
private static _leopardMutex = new Mutex();
Example #28
Source File: leopard.ts    From leopard with Apache License 2.0 5 votes vote down vote up
private readonly _processMutex: Mutex;
Example #29
Source File: endpoint-client.test.ts    From smartthings-core-sdk with Apache License 2.0 4 votes vote down vote up
describe('EndpointClient',  () => {
	const mockRequest = axios.request as jest.Mock<Promise<AxiosResponse>, [AxiosRequestConfig]>
	mockRequest.mockResolvedValue({ status: 200, data: { status: 'ok' } } as AxiosResponse)

	class TokenStore implements RefreshTokenStore {
		public authData?: AuthData
		getRefreshData(): Promise<RefreshData> {
			return Promise.resolve({ refreshToken: 'xxx', clientId: 'aaa', clientSecret: 'bbb' })
		}
		putAuthData (data: AuthData): Promise<void> {
			this.authData = data
			return Promise.resolve()
		}
	}

	const tokenStore = new TokenStore()
	const token = 'authToken'

	let client: EndpointClient

	const configWithoutHeaders = {
		urlProvider: defaultSmartThingsURLProvider,
		authenticator: new RefreshTokenAuthenticator(token, tokenStore),
		baseURL: 'https://api.smartthings.com',
		authURL: 'https://auth.smartthings.com',
	}
	const headers = {
		'Content-Type': 'application/json;charset=utf-8',
		Accept: 'application/json',
	}
	const buildClient = (config: EndpointClientConfig = { ...configWithoutHeaders, headers }): EndpointClient =>
		new EndpointClient('base/path', { ...config, headers: { ...config.headers }})

	beforeEach(() => {
		client = buildClient()
	})

	afterEach(() => {
		jest.clearAllMocks()
	})

	describe('setHeader', () => {
		it('adds header to config', () => {
			client.setHeader('NewHeader', 'header value')

			expect(client.config.headers?.NewHeader).toBe('header value')
		})

		it('works when no previous headers set', () => {
			const client = new EndpointClient('base/path', { ...configWithoutHeaders })
			client.setHeader('NewHeader', 'header value')

			expect(client.config.headers?.NewHeader).toBe('header value')
		})
	})

	describe('removeHeader', () => {
		it('removes header from config', () => {
			client.setHeader('NewHeader', 'header value')

			expect(client.config.headers?.NewHeader).toBe('header value')

			client.removeHeader('NewHeader')

			expect(client.config.headers?.NewHeader).toBeUndefined()
		})

		it('ignores undefined headers', () => {
			client.removeHeader('NewHeader')

			expect(client.config.headers?.NewHeader).toBeUndefined()
		})
	})

	describe('request', () => {
		it('submits basic request', async () => {
			const response = await client.request('GET', 'my/path')

			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'GET',
				headers: {
					...headers,
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')

			const stringifyMock = (qs.stringify as jest.Mock<string, [unknown]>)
				.mockReturnValue('stringified parameters')
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			const paramsSerializer = mockRequest.mock.calls[0][0].paramsSerializer as (params: any) => string

			expect(paramsSerializer).toBeDefined()
			expect(paramsSerializer({ param: 'value' })).toBe('stringified parameters')

			expect(stringifyMock).toHaveBeenCalledTimes(1)
			expect(stringifyMock).toHaveBeenCalledWith({ param: 'value' }, { indices: false })
		})

		it('adds accept header for version', async () => {
			const client = buildClient({
				...configWithoutHeaders,
				version: 'api-version',
				headers: { Accept: 'accept-header' },
			})
			const response = await client.request('GET', 'my/path')

			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'GET',
				headers: {
					Accept: 'application/vnd.smartthings+json;v=api-version, accept-header',
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('includes version along with accept header', async () => {
			const client = buildClient({ ...configWithoutHeaders, version: 'api-version' })
			const response = await client.request('GET', 'my/path')

			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'GET',
				headers: {
					Accept: 'application/vnd.smartthings+json;v=api-version',
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('adds header overrides to headers submitted', async () => {
			const headerOverrides = {
				'Content-Type': 'overridden content type',
				'X-ST-Organization': '00000000-0000-0000-0000-000000000008',
			}
			const response = await client.request('POST', 'my/path', { name: 'Bob' }, undefined, { headerOverrides })
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'POST',
				headers: {
					'Content-Type': 'overridden content type',
					Accept: 'application/json',
					Authorization: `Bearer ${token}`,
					'X-ST-Organization': '00000000-0000-0000-0000-000000000008',
				},
				data: { name: 'Bob' },
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('includes logging id when specified', async () => {
			const client = buildClient({ ...configWithoutHeaders, loggingId: 'request-logging-id' })
			const response = await client.request('GET', 'my/path')
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'GET',
				headers: {
					Authorization: `Bearer ${token}`,
					'X-ST-CORRELATION': 'request-logging-id',
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('calls warningLogger when included and needed', async () => {
			const warningLogger = jest.fn()
			client = buildClient({ ...configWithoutHeaders, warningLogger })
			mockRequest.mockResolvedValueOnce({
				status: 200,
				data: { status: 'ok' },
				headers: { warning: '299 - "Danger, Will Robinson! Danger!"' },
			} as AxiosResponse)
			const response = await client.request('GET', 'my/path')
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'GET',
				headers: {
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')

			expect(warningLogger).toHaveBeenCalledTimes(1)
			expect(warningLogger).toHaveBeenCalledWith([{
				code: 299,
				agent: '-',
				text: 'Danger, Will Robinson! Danger!',
			}])
		})

		it('is okay with no warningLogger', async () => {
			mockRequest.mockResolvedValueOnce({
				status: 200,
				data: { status: 'ok' },
				headers: { warning: 'warning message in header' },
			} as AxiosResponse)

			expect(client.request('GET', 'my/path')).resolves.not.toThrow
		})

		it('returns dryRunReturnValue in dry run mode', async () => {
			const dryRunReturnValue = { usually: 'very similar to the input' }
			const response = await client.request('GET', 'my/path', undefined, undefined, {
				dryRun: true,
				dryRunReturnValue,
			})
			expect(mockRequest).toHaveBeenCalledTimes(0)
			expect(response).toBe(dryRunReturnValue)
		})

		it('throws error in dry run mode when return value not specified', async () => {
			await expect(client.request('GET', 'my/path', undefined, undefined, {
				dryRun: true,
			})).rejects.toThrow('skipping request; dry run mode')
			expect(mockRequest).toHaveBeenCalledTimes(0)
		})
	})

	describe('get', () => {
		it('submits basic request', async () => {
			const response = await client.get('path2')
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/path2',
				method: 'get',
				headers: {
					...headers,
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('includes query params when specified', async () => {
			const response = await client.get('my/path', { locationId: 'XXX' })
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base/path/my/path',
				method: 'get',
				headers: {
					...headers,
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: {
					locationId: 'XXX',
				},
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('skips base path with absolute path', async () => {
			const response = await client.get('/base2/this/path')
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/base2/this/path',
				method: 'get',
				headers: {
					...headers,
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})

		it('skips base URL and path with absolute URL', async () => {
			const response = await client.get('https://api.smartthings.com/absolute/url')
			expect(mockRequest).toHaveBeenCalledTimes(1)
			expect(mockRequest).toHaveBeenCalledWith({
				url: 'https://api.smartthings.com/absolute/url',
				method: 'get',
				headers: {
					...headers,
					Authorization: `Bearer ${token}`,
				},
				data: undefined,
				params: undefined,
				paramsSerializer: expect.any(Function),
			})
			expect(response.status).toBe('ok')
		})
	})

	test('post', async () => {
		const response = await client.post('myotherpath', { name: 'Bill' })
		expect(mockRequest).toHaveBeenCalledTimes(1)
		expect(mockRequest).toHaveBeenCalledWith({
			url: 'https://api.smartthings.com/base/path/myotherpath',
			method: 'post',
			headers: {
				...headers,
				Authorization: `Bearer ${token}`,
			},
			data: {
				name: 'Bill',
			},
			params: undefined,
			paramsSerializer: expect.any(Function),
		})
		expect(response.status).toBe('ok')
	})

	test('put', async () => {
		const response = await client.put('myotherpath', { name: 'Bill' })
		expect(mockRequest).toHaveBeenCalledTimes(1)
		expect(mockRequest).toHaveBeenCalledWith({
			url: 'https://api.smartthings.com/base/path/myotherpath',
			method: 'put',
			headers: {
				...headers,
				Authorization: `Bearer ${token}`,
			},
			data: {
				name: 'Bill',
			},
			params: undefined,
			paramsSerializer: expect.any(Function),
		})
		expect(response.status).toBe('ok')
	})

	test('patch', async () => {
		const response = await client.patch('path3', { name: 'Joe' })
		expect(mockRequest).toHaveBeenCalledTimes(1)
		expect(mockRequest).toHaveBeenCalledWith({
			url: 'https://api.smartthings.com/base/path/path3',
			method: 'patch',
			headers: {
				...headers,
				Authorization: `Bearer ${token}`,
			},
			data: {
				name: 'Joe',
			},
			params: undefined,
			paramsSerializer: expect.any(Function),
		})
		expect(response.status).toBe('ok')
	})

	test('delete', async () => {
		const response = await client.delete('path3')
		expect(mockRequest).toHaveBeenCalledTimes(1)
		expect(mockRequest).toHaveBeenCalledWith({
			url: 'https://api.smartthings.com/base/path/path3',
			method: 'delete',
			headers: {
				...headers,
				Authorization: `Bearer ${token}`,
			},
			data: undefined,
			params: undefined,
			paramsSerializer: expect.any(Function),
		})
		expect(response.status).toBe('ok')
	})

	test('expired token request', async () => {
		mockRequest
			.mockImplementationOnce(() => Promise.reject(
				{ response: {status: 401, data: 'Unauthorized'} }))
			.mockImplementationOnce(() => Promise.resolve(
				{ status: 200, data: { access_token: 'my-access-token', refresh_token: 'my-refresh-token' } } as AxiosResponse))
			.mockImplementationOnce(() => Promise.resolve(
				{ status: 200, data: { status: 'ok' } } as AxiosResponse))

		const response = await client.get('my/path')
		expect(mockRequest).toHaveBeenCalledTimes(3)
		expect(response.status).toBe('ok')
	})

	test('expired token request with mutex', async () => {
		// TODO -- actually test mutex??
		const mutex = new Mutex()
		const mutexConfig = {
			authenticator: new SequentialRefreshTokenAuthenticator(token, tokenStore, mutex),
			baseURL: 'https://api.smartthings.com',
			authURL: 'https://auth.smartthings.com',
			headers: { ...headers },
		}
		const mutexClient = buildClient(mutexConfig)

		mockRequest
			.mockImplementationOnce(() => Promise.reject(
				{response: { status: 401, data: 'Unauthorized' }}))
			.mockImplementationOnce(() => Promise.resolve(
				{ status: 200, data: { access_token: 'my-access-token', refresh_token: 'my-refresh-token' } } as AxiosResponse))
			.mockImplementationOnce(() => Promise.resolve(
				{ status: 200, data: { status: 'ok' } } as AxiosResponse))

		const response = await mutexClient.get('my/path')
		expect(mockRequest).toHaveBeenCalledTimes(3)
		expect(response.status).toBe('ok')
	})

	test('get 404', async () => {
		mockRequest.mockImplementationOnce(() => Promise.reject({response: { status: 404, data: 'Not Found' }}))
		let threwError = false
		try {
			await client.get('path2')
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} catch (error: any) {
			expect(error.response.status).toBe(404)
			threwError = true
		}
		expect(mockRequest).toHaveBeenCalledTimes(1)
		expect(threwError).toBe(true)
	})

	test('get refresh fail', async () => {
		mockRequest
			.mockImplementationOnce(() => Promise.reject({response: { status: 401, data: 'Unauthorized' }}))
			.mockImplementationOnce(() => Promise.reject({response: { status: 500, data: 'Server error' }}))

		let threwError = false
		try {
			await client.get('path2')
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} catch (error: any) {
			expect(error.response.status).toBe(500)
			threwError = true
		}
		expect(mockRequest).toHaveBeenCalledTimes(2)
		expect(threwError).toBe(true)
	})

	describe('request logging', () => {
		jest.spyOn(NoLogLogger.prototype, 'isDebugEnabled').mockReturnValue(true)
		const debugSpy = jest.spyOn(NoLogLogger.prototype, 'debug')

		it('partially redacts bearer token in request log', async () => {
			const bearerToken = '00000000-0000-0000-0000-000000000000'
			const config: EndpointClientConfig = {
				authenticator: new BearerTokenAuthenticator(bearerToken),
				logger: new NoLogLogger,
			}
			const bearerClient = new EndpointClient('basePath', config)

			await bearerClient.get('')

			expect(debugSpy).toBeCalled()
			expect(debugSpy).not.toBeCalledWith(expect.stringContaining(bearerToken))
			expect(debugSpy).toBeCalledWith(expect.stringContaining('Bearer 00000000'))
		})

		it('fully redacts Auth header when Bearer is not present', async () => {
			const basicAuth = Buffer.from('username:password', 'ascii').toString('base64')
			class BasicAuthenticator implements Authenticator {
				authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
					return Promise.resolve({
						...requestConfig,
						headers: {
							...requestConfig.headers,
							Authorization: `Basic ${basicAuth}`,
						},
					})
				}
			}
			const config: EndpointClientConfig = {
				authenticator: new BasicAuthenticator,
				logger: new NoLogLogger(),
			}
			const basicClient = new EndpointClient('basePath', config)

			await basicClient.get('')

			expect(debugSpy).toBeCalled()
			expect(debugSpy).not.toBeCalledWith(expect.stringContaining(basicAuth))
			expect(debugSpy).toBeCalledWith(expect.stringContaining('Authorization":"(redacted)"'))
		})

		describe('getPagedItems', () => {
			const getMock = jest.fn()
			const client = new EndpointClient('paged-thing', {
				authenticator: new NoOpAuthenticator(),
				logger: new NoLogLogger(),
			})
			client.get = getMock

			const item1 = { name: 'item-1' }
			const item2 = { name: 'item-2' }

			it('uses single get when full results returned in one go', async () => {
				getMock.mockResolvedValueOnce({
					items: [item1, item2],
				})

				expect(await client.getPagedItems()).toEqual([item1, item2])

				expect(getMock).toHaveBeenCalledTimes(1)
				expect(getMock).toHaveBeenCalledWith(undefined, undefined, undefined)
			})

			it('combines multiple pages', async () => {
				const params = { paramName: 'param-value' }
				const options = { dryRun: false }
				getMock
					.mockResolvedValueOnce({ items: [item1], _links: { next: { href: 'next-url' } } })
					.mockResolvedValueOnce({ items: [item2] })

				expect(await client.getPagedItems('first-url', params, options)).toEqual([item1, item2])

				expect(getMock).toHaveBeenCalledTimes(2)
				expect(getMock).toHaveBeenCalledWith('first-url', params, options)
				expect(getMock).toHaveBeenCalledWith('next-url', undefined, options)
			})
		})
	})
})