Skip to content

Commit

Permalink
feat: wait for plan selection (#1547)
Browse files Browse the repository at this point in the history
# Add `wait()` Method to `AccountPlan` Class

## Summary

This PR introduces a new `wait()` method to the `AccountPlan` class,
which simplifies the process of waiting for a payment plan to be
selected. By integrating configurable polling intervals, timeouts, and
abort signals, the method abstracts the common logic required in this
scenario, making it easier for developers to implement and customize
their workflows.

## Changes

- **New `wait()` Method**:
- The `wait()` method repeatedly checks the account's payment plan
status at a given interval until:
    - A valid plan is selected.
    - The timeout is reached.
    - The operation is aborted using an `AbortSignal`.
  - Available options:
    - `interval` (default: 1000ms): Sets the polling interval.
    - `timeout` (default: 15 minutes): Defines the maximum wait time.
    - `signal`: An optional `AbortSignal` to allow for cancellation.
- This method improves the developer experience by abstracting the
repetitive logic required to wait for a payment plan and offering
flexibility through configurable options.

## How to Test

1. Run the test suite with `npm run build && npm run test`.
2. Ensure all existing tests pass.
3. Verify that the new test cases for the `wait()` method in
`account.test.js` are passing, including tests for:
   - Successful payment plan selection.
   - Timeout errors.
   - Aborted operations.
  • Loading branch information
fforbeck authored Sep 16, 2024
1 parent ea02adb commit 7fdee77
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 7 deletions.
9 changes: 2 additions & 7 deletions packages/w3up-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,8 @@ const account = await client.login('zaphod@beeblebrox.galaxy')
If your account doesn't have a payment plan yet, you'll be prompted to select one after verifying your email. A payment plan is required to provision a space. You can use the following loop to wait until a payment plan is selected:

```js
// wait for payment plan to be selected
while (true) {
const res = await account.plan.get()
if (res.ok) break
console.log('Waiting for payment plan to be selected...')
await new Promise((resolve) => setTimeout(resolve, 1000))
}
// Wait for a payment plan with a 1-second polling interval and 15-minute timeout
await account.plan.wait()
```

Spaces can be created using the [`createSpace` client method][docs-client#createSpace]:
Expand Down
40 changes: 40 additions & 0 deletions packages/w3up-client/src/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,46 @@ export class AccountPlan {
})
}

/**
* Waits for a payment plan to be selected.
* This method continuously checks the account's payment plan status
* at a specified interval until a valid plan is selected, or when the timeout is reached,
* or when the abort signal is aborted.
*
* @param {object} [options]
* @param {number} [options.interval=1000] - The polling interval in milliseconds (default is 1000ms).

Check warning on line 245 in packages/w3up-client/src/account.js

View workflow job for this annotation

GitHub Actions / Test (18)

Defaults are not permitted on @param

Check warning on line 245 in packages/w3up-client/src/account.js

View workflow job for this annotation

GitHub Actions / Test (20)

Defaults are not permitted on @param
* @param {number} [options.timeout=900000] - The maximum time to wait in milliseconds before throwing a timeout error (default is 15 minutes).

Check warning on line 246 in packages/w3up-client/src/account.js

View workflow job for this annotation

GitHub Actions / Test (18)

Defaults are not permitted on @param

Check warning on line 246 in packages/w3up-client/src/account.js

View workflow job for this annotation

GitHub Actions / Test (20)

Defaults are not permitted on @param
* @param {AbortSignal} [options.signal] - An optional AbortSignal to cancel the waiting process.
* @returns {Promise<import('@web3-storage/access').PlanGetSuccess>} - Resolves once a payment plan is selected within the timeout.
* @throws {Error} - Throws an error if there is an issue retrieving the payment plan or if the timeout is exceeded.
*/
async wait(options) {
const startTime = Date.now()
const interval = options?.interval || 1000 // 1 second
const timeout = options?.timeout || 60 * 15 * 1000 // 15 minutes

// eslint-disable-next-line no-constant-condition
while (true) {
const res = await this.get()
if (res.ok) return res.ok

if (res.error) {
throw new Error(`Error retrieving payment plan: ${res.error}`)
}

if (Date.now() - startTime > timeout) {
throw new Error('Timeout: Payment plan selection took too long.')
}

if (options?.signal?.aborted) {
throw new Error('Aborted: Payment plan selection was aborted.')
}

console.log('Waiting for payment plan to be selected...')
await new Promise((resolve) => setTimeout(resolve, interval))
}
}

/**
*
* @param {import('@web3-storage/access').AccountDID} accountDID
Expand Down
85 changes: 85 additions & 0 deletions packages/w3up-client/test/account.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,91 @@ export const testAccount = Test.withContext({

assert.deepEqual(client.currentSpace()?.did(), space.did())
},

waitForPaymentPlan: {
'should wait for a payment plan to be selected': async (
assert,
{ client, mail, grantAccess }
) => {
const email = 'alice@web.mail'
const login = Account.login(client, email)
await grantAccess(await mail.take())
const account = Result.try(await login)

let callCount = 0
// Mock the get method to simulate a plan being selected after some time
// @ts-expect-error
account.plan.get = async () => {
callCount++
if (callCount > 2) {
return { ok: { product: 'did:web:example.com' } }
}
return { ok: false }
}

const result = await account.plan.wait({ interval: 100, timeout: 1000 })
assert.deepEqual(result.product, 'did:web:example.com')
},

'should throw an error if there is an issue retrieving the payment plan':
async (assert, { client, mail, grantAccess }) => {
const email = 'alice@web.mail'
const login = Account.login(client, email)
await grantAccess(await mail.take())
const account = Result.try(await login)

// @ts-expect-error
account.plan.get = async () => Promise.resolve({ error: 'Some error' })

await assert.rejects(
account.plan.wait({ interval: 100, timeout: 1000 }),
{
message: 'Error retrieving payment plan: Some error',
}
)
},

'should throw a timeout error if the payment plan selection takes too long':
async (assert, { client, mail, grantAccess }) => {
const email = 'alice@web.mail'
const login = Account.login(client, email)
await grantAccess(await mail.take())
const account = Result.try(await login)

// @ts-expect-error
account.plan.get = async () => Promise.resolve({ ok: false })

await assert.rejects(
account.plan.wait({ interval: 100, timeout: 500 }),
{
message: 'Timeout: Payment plan selection took too long.',
}
)
},

'should throw an error when the abort signal is aborted': async (
assert,
{ client, mail, grantAccess }
) => {
const abortController = new AbortController()
const signal = abortController.signal

const email = 'alice@web.mail'
const login = Account.login(client, email)
await grantAccess(await mail.take())
const account = Result.try(await login)

// @ts-expect-error
account.plan.get = async () => Promise.resolve({ ok: false })

// Abort the signal after a short delay
setTimeout(() => abortController.abort(), 100)

await assert.rejects(account.plan.wait({ signal }), {
message: 'Aborted: Payment plan selection was aborted.',
})
},
},
})

Test.test({ Account: testAccount })

0 comments on commit 7fdee77

Please sign in to comment.