Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: enable strict type checking #80

Merged
merged 34 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c1ecd45
add strict type checking
Feb 4, 2025
f3621d6
moved slug
Feb 4, 2025
ca64e6d
fmt
Feb 4, 2025
819d822
Merge branch 'main' into nhaimerl-activate-strict-type-checking
NikolasHaimerl Feb 4, 2025
ec40349
commitMerge branch 'main' into nhaimerl-activate-strict-type-checking
Feb 4, 2025
a2f4e92
use clam event
Feb 4, 2025
71b844a
Merge branch 'nhaimerl-activate-strict-type-checking' of https://gith…
Feb 4, 2025
6861130
fmt
Feb 5, 2025
3882f4b
Merge branch 'main' into nhaimerl-activate-strict-type-checking
Feb 5, 2025
194bdca
merged with main
Feb 5, 2025
e1a7484
add SubmittableDeal type
Feb 11, 2025
5e82c24
Merge branch 'main' of https://github.com/filecoin-station/deal-observer
Feb 11, 2025
2072510
Revert "add SubmittableDeal type"
Feb 12, 2025
02c3e3c
Merge branch 'main' of https://github.com/filecoin-station/deal-observer
Feb 12, 2025
9f0d665
Merge branch 'main' of https://github.com/filecoin-station/deal-observer
Feb 14, 2025
8e1cb93
introduce fully typed MakeRpcRequest
bajtos Feb 21, 2025
cddc0ca
introduce shared type alias GetDealPayloadCid
bajtos Feb 21, 2025
7fab2e9
introduce UnknownRow and QueryResultWithUnknownRows
bajtos Feb 21, 2025
c8dc5f9
merged with main
Feb 21, 2025
7463a11
chore: add typings file
Feb 21, 2025
43e1337
fix: remove use of any
Feb 21, 2025
f73e3d8
chore: formatting
Feb 21, 2025
d54f025
chore: formatting
Feb 21, 2025
4cc2e50
fix MakeRpcRequest
bajtos Feb 21, 2025
eda4914
fix tsc errors after making MakeRpcRequest stricter
bajtos Feb 21, 2025
fddbd91
remove `any` from `decodeCborInBase64`
bajtos Feb 21, 2025
dffd3cd
rpcRequest accepts an array as params
bajtos Feb 21, 2025
5769366
Update backend/lib/rpc-service/service.js
NikolasHaimerl Feb 21, 2025
977f83e
Merge branch 'main' of https://github.com/filecoin-station/deal-observer
Feb 23, 2025
dd4a886
Update backend/lib/deal-observer.js
NikolasHaimerl Feb 24, 2025
b78a422
Update backend/lib/resolve-payload-cids.js
NikolasHaimerl Feb 24, 2025
5f61e22
merged with main
Feb 24, 2025
d2e553c
Merge branch 'main' of https://github.com/filecoin-station/deal-observer
Feb 24, 2025
3db1d21
merged with main
Feb 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/test/test-helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AssertionError } from 'node:assert'

/**
* @param {Response} res
* @param {number} status
*/
export const assertResponseStatus = async (res, status) => {
if (res.status !== status) {
throw new AssertionError({
Expand Down
14 changes: 12 additions & 2 deletions backend/bin/deal-observer-backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { fetchDealWithHighestActivatedEpoch, countStoredActiveDeals, observeBuil
import { indexPieces } from '../lib/piece-indexer.js'
import { findAndSubmitUnsubmittedDeals, submitDealsToSparkApi } from '../lib/spark-api-submit-deals.js'
import { getDealPayloadCid } from '../lib/piece-indexer-service.js'
/** @import {Queryable} from '@filecoin-station/deal-observer-db' */

const {
INFLUXDB_TOKEN,
Expand All @@ -37,6 +38,10 @@ assert(finalityEpochs <= maxPastEpochs)
const pgPool = await createPgPool()
const { recordTelemetry } = createInflux(INFLUXDB_TOKEN)

/**
* @param {(method:string,params:any[]) => Promise<any>} makeRpcRequest
* @param {Queryable} pgPool
*/
const observeActorEventsLoop = async (makeRpcRequest, pgPool) => {
const LOOP_NAME = 'Observe actor events'
while (true) {
Expand All @@ -46,7 +51,7 @@ const observeActorEventsLoop = async (makeRpcRequest, pgPool) => {
const lastInsertedDeal = await fetchDealWithHighestActivatedEpoch(pgPool)
const startEpoch = Math.max(
currentChainHead.Height - maxPastEpochs,
(lastInsertedDeal?.activated_at_epoch + 1) || 0
lastInsertedDeal ? (lastInsertedDeal.activated_at_epoch ?? -1) + 1 : 0
)
const endEpoch = currentChainHead.Height - finalityEpochs

Expand All @@ -57,7 +62,7 @@ const observeActorEventsLoop = async (makeRpcRequest, pgPool) => {
const numberOfStoredDeals = await countStoredActiveDeals(pgPool)
if (INFLUXDB_TOKEN) {
recordTelemetry('observed_deals_stats', point => {
point.intField('last_searched_epoch', newLastInsertedDeal.activated_at_epoch)
point.intField('last_searched_epoch', newLastInsertedDeal?.activated_at_epoch || 0)
point.intField('number_of_stored_active_deals', numberOfStoredDeals)
})
}
Expand Down Expand Up @@ -126,6 +131,11 @@ const sparkApiSubmitDealsLoop = async (pgPool, { sparkApiBaseUrl, sparkApiToken,
}
}

/**
* @param {(method:string,params:object) => object} makeRpcRequest
* @param {(providerId:string,pieceCid:string) => Promise<string|null>} getDealPayloadCid
* @param {*} pgPool
*/
export const pieceIndexerLoop = async (makeRpcRequest, getDealPayloadCid, pgPool) => {
const LOOP_NAME = 'Piece Indexer'
while (true) {
Expand Down
6 changes: 3 additions & 3 deletions backend/lib/deal-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { convertBlockEventToActiveDealDbEntry } from './utils.js'
/**
* @param {number} blockHeight
* @param {Queryable} pgPool
* @param {(method:string,params:object) => object} makeRpcRequest
* @param {(method:string,params:any[]) => Promise<any>} makeRpcRequest
* @returns {Promise<void>}
*/
export async function observeBuiltinActorEvents (blockHeight, pgPool, makeRpcRequest) {
Expand Down Expand Up @@ -94,11 +94,11 @@ export async function storeActiveDeals (activeDeals, pgPool) {
/**
* @param {Queryable} pgPool
* @param {string} query
* @param {Array} args
* @param {Array<number | string>} args
* @returns {Promise<Array<Static <typeof ActiveDealDbEntry>>>}
*/
export async function loadDeals (pgPool, query, args = []) {
const result = (await pgPool.query(query, args)).rows.map(deal => {
const result = (await pgPool.query(query, args)).rows.map((/** @type {any} */ deal) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deal is validated shortly after this call using typebox. This means that it is safe to use any type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos what do you think about introducing a new type, called ToBeParsed or similar, which equals any? This way we don't have any any, and can forbid it, but at the same time, signal that this type needs to be parsed, and will be parsed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use Record<string, unknown>. I wish node-postgres was using that type for result.rows out of the box. (I'll handle this myself and push a new commit.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am introducing UnknownRow type that we can share. I am happy to discuss a better name for this type.

Copy link
Member

@bajtos bajtos Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you can do pgPool.query<UnknownRow>(...) in TypeScript, but that's unfortunately not possible with JSDoc-style typings :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversation is resolved as far as I am concerned. I propose to start a new thread if you disagree with some of the decisions I made in 7fab2e9

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversation is resolved as far as I am concerned. I propose to start a new thread if you disagree with some of the decisions I made in 7fab2e9

After merging with main, we now can use SubmittableDeals

const SubmittableDeal = Type.Object({
, which I believe is a better alternative to unkown types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversation is resolved as far as I am concerned. I propose to start a new thread if you disagree with some of the decisions I made in 7fab2e9

After merging with main, we now can use SubmittableDeals

const SubmittableDeal = Type.Object({

, which I believe is a better alternative to unkown types.

I agree it's a better alternative than unknown 👍🏻

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos can we disallow any in typescript settings?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos can we disallow any in typescript settings?

Not as far as I know.

There is a typescript-eslint rule no-explicit-any, but IIRC, typescript-eslint does not work with JSDoc-in-JS-files, plus we are using standard.js.

I am going to mention this topic here:

// SQL used null, typebox needs undefined for null values
Object.keys(deal).forEach(key => {
if (deal[key] === null) {
Expand Down
9 changes: 8 additions & 1 deletion backend/lib/rpc-service/data-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ const RpcRespone = Type.Object({
result: Type.Any()
})

const ChainHead = Type.Object({
Height: Type.Number(),
Blocks: Type.Any(),
Cids: Type.Any()
})

export {
ClaimEvent,
Entry,
RawActorEvent,
BlockEvent,
RpcRespone
RpcRespone,
ChainHead
}
16 changes: 11 additions & 5 deletions backend/lib/rpc-service/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { encode as cborEncode } from '@ipld/dag-cbor'
import { rawEventEntriesToEvent } from './utils.js'
import { Value } from '@sinclair/typebox/value'
import * as util from 'node:util'
import { ClaimEvent, RawActorEvent, BlockEvent, RpcRespone } from './data-types.js'
import { ClaimEvent, RawActorEvent, BlockEvent, RpcRespone, ChainHead } from './data-types.js'
import pRetry from 'p-retry'
/** @import { Static } from '@sinclair/typebox' */

Expand Down Expand Up @@ -40,8 +40,9 @@ export const rpcRequest = async (method, params) => {
}
}
/**
* @param {object} actorEventFilter
* @param {{fromHeight:number,toHeight:number,fields: any}} actorEventFilter
* Returns actor events filtered by the given actorEventFilter
* @param {(method: string, params: any[]) => Promise<any>} makeRpcRequest
* @returns {Promise<Array<Static<typeof BlockEvent>>>}
*/
export async function getActorEvents (actorEventFilter, makeRpcRequest) {
Expand All @@ -52,7 +53,7 @@ export async function getActorEvents (actorEventFilter, makeRpcRequest) {
}
// TODO: handle reverted events
// https://github.com/filecoin-station/deal-observer/issues/22
const typedRawEventEntries = rawEvents.map((rawEvent) => Value.Parse(RawActorEvent, rawEvent))
const typedRawEventEntries = rawEvents.map((/** @type {any} */ rawEvent) => Value.Parse(RawActorEvent, rawEvent))
// An emitted event contains the height at which it was emitted, the emitter and the event itself
const emittedEvents = []
for (const typedEventEntries of typedRawEventEntries) {
Expand Down Expand Up @@ -81,10 +82,15 @@ export async function getActorEvents (actorEventFilter, makeRpcRequest) {

/**
* @param {function} makeRpcRequest
* @returns {Promise<object>}
* @returns {Promise<Static<typeof ChainHead>>}
*/
export async function getChainHead (makeRpcRequest) {
return await makeRpcRequest('Filecoin.ChainHead', [])
const result = await makeRpcRequest('Filecoin.ChainHead', [])
try {
return Value.Parse(ChainHead, result)
} catch (e) {
throw Error(util.format('Failed to parse chain head: %o', result))
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions backend/lib/rpc-service/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { base64pad } from 'multiformats/bases/base64'
import { decode as cborDecode } from '@ipld/dag-cbor'
import * as util from 'node:util'

/**
* @param {string} data
* @returns
*/
const decodeCborInBase64 = (data) => {
return cborDecode(base64pad.baseDecode(data))
}
Expand All @@ -14,6 +18,7 @@ const decodeCborInBase64 = (data) => {
*/
const rawEventEntriesToEvent = (rawEventEntries) => {
// Each event is defined by a list of event entries which will parsed into a typed event
/** @type {Record<string, any>} */
const event = {}
let eventType
for (const entry of rawEventEntries) {
Expand Down
8 changes: 4 additions & 4 deletions backend/lib/spark-api-submit-deals.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as Sentry from '@sentry/node'
*
* @param {PgPool} pgPool
* @param {number} batchSize
* @param {(eligibleDeals: Array) => Promise<{ingested: number; skipped: number}>} submitDeals
* @param {(eligibleDeals: Array<any>) => Promise<{ingested: number; skipped: number}>} submitDeals
* @returns {Promise<{submitted: number; ingested: number; skipped: number;}>} Number of deals submitted, ingested and skipped
*/
export const findAndSubmitUnsubmittedDeals = async (pgPool, batchSize, submitDeals) => {
Expand Down Expand Up @@ -45,7 +45,7 @@ export const findAndSubmitUnsubmittedDeals = async (pgPool, batchSize, submitDea
*
* @param {PgPool} pgPool
* @param {number} batchSize
* @returns {AsyncGenerator<Array>}
* @returns {AsyncGenerator<Array<any>>}
*/
const findUnsubmittedDeals = async function * (pgPool, batchSize) {
const client = await pgPool.connect()
Expand Down Expand Up @@ -82,7 +82,7 @@ const findUnsubmittedDeals = async function * (pgPool, batchSize) {
* Mark deals as submitted.
*
* @param {Queryable} pgPool
* @param {Array} eligibleDeals
* @param {Array<any>} eligibleDeals
*/
const markDealsAsSubmitted = async (pgPool, eligibleDeals) => {
await pgPool.query(`
Expand Down Expand Up @@ -112,7 +112,7 @@ const markDealsAsSubmitted = async (pgPool, eligibleDeals) => {
*
* @param {string} sparkApiBaseURL
* @param {string} sparkApiToken
* @param {Array} deals
* @param {Array<any>} deals
* @returns {Promise<{ingested: number; skipped: number}>}
*/
export const submitDealsToSparkApi = async (sparkApiBaseURL, sparkApiToken, deals) => {
Expand Down
4 changes: 4 additions & 0 deletions backend/lib/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import createDebug from 'debug'

const debug = createDebug('spark:deal-observer:telemetry')

/**
* @param {string | undefined} token
* @returns {{influx: InfluxDB,recordTelemetry: (name: string, fn: (p: Point) => void) => void}}
*/
export const createInflux = token => {
const influx = new InfluxDB({
url: 'https://eu-central-1-1.aws.cloud2.influxdata.com',
Expand Down
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"test": "node --test --test-reporter=spec --test-concurrency=1"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/pg-cursor": "^2.7.2",
"@types/slug": "^5.0.9",
"standard": "^17.1.2"
},
"dependencies": {
Expand Down
14 changes: 11 additions & 3 deletions backend/test/deal-observer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { after, before, beforeEach, describe, it } from 'node:test'
import { createPgPool, migrateWithPgClient } from '@filecoin-station/deal-observer-db'
import { fetchDealWithHighestActivatedEpoch, countStoredActiveDeals, loadDeals, storeActiveDeals } from '../lib/deal-observer.js'
import { Value } from '@sinclair/typebox/value'
import { BlockEvent } from '../lib/rpc-service/data-types.js'
import { BlockEvent, ClaimEvent } from '../lib/rpc-service/data-types.js'
import { convertBlockEventToActiveDealDbEntry } from '../lib/utils.js'
/** @import {PgPool} from '@filecoin-station/deal-observer-db' */
/** @import { Static } from '@sinclair/typebox' */

describe('deal-observer-backend', () => {
/**
* @type {PgPool}
*/
let pgPool
before(async () => {
pgPool = await createPgPool()
Expand Down Expand Up @@ -74,12 +79,15 @@ describe('deal-observer-backend', () => {
})

it('check number of stored deals', async () => {
/**
* @param {Static<typeof ClaimEvent>} eventData
*/
const storeBlockEvent = async (eventData) => {
const event = Value.Parse(BlockEvent, { height: 1, event: eventData, emitter: 'f06' })
const dbEntry = convertBlockEventToActiveDealDbEntry(event)
await storeActiveDeals([dbEntry], pgPool)
}
const data = {
const data = Value.Parse(ClaimEvent, {
id: 1,
provider: 2,
client: 3,
Expand All @@ -89,7 +97,7 @@ describe('deal-observer-backend', () => {
termMin: 12340,
termMax: 12340,
sector: 6n
}
})
assert.strictEqual(await countStoredActiveDeals(pgPool), 0n)
await storeBlockEvent(data)
assert.strictEqual(await countStoredActiveDeals(pgPool), 1n)
Expand Down
16 changes: 15 additions & 1 deletion backend/test/piece-indexer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,29 @@ import assert from 'assert'
import { minerPeerIds } from './test_data/minerInfo.js'
import { payloadCIDs } from './test_data/payloadCIDs.js'
import { indexPieces } from '../lib/piece-indexer.js'
/** @import {PgPool} from '@filecoin-station/deal-observer-db' */

describe('deal-observer-backend piece indexer', () => {
/**
* @param {string} method
* @param {any[]} params
* @returns
*/
const makeRpcRequest = async (method, params) => {
switch (method) {
case 'Filecoin.ChainHead':
return parse(JSON.stringify(chainHeadTestData))
case 'Filecoin.GetActorEventsRaw':
return parse(JSON.stringify(rawActorEventTestData)).filter(e => e.height >= params[0].fromHeight && e.height <= params[0].toHeight)
return parse(JSON.stringify(rawActorEventTestData)).filter((/** @type {{ height: number; }} */ e) => e.height >= params[0].fromHeight && e.height <= params[0].toHeight)
case 'Filecoin.StateMinerInfo':
return minerPeerIds.get(params[0])
default:
console.error('Unknown method')
}
}
/**
* @type {PgPool}
*/
let pgPool
before(async () => {
pgPool = await createPgPool()
Expand All @@ -46,6 +55,11 @@ describe('deal-observer-backend piece indexer', () => {

it('piece indexer loop function fetches deals where there exists no payload yet and updates the database entry', async (t) => {
const getDealPayloadCidCalls = []
/**
* @param {number} providerId
* @param {string} pieceCid
* @returns {Promise<string | undefined>}
*/
const getDealPayloadCid = async (providerId, pieceCid) => {
getDealPayloadCidCalls.push({ providerId, pieceCid })
const payloadCid = payloadCIDs.get(JSON.stringify({ minerId: providerId, pieceCid }))
Expand Down
10 changes: 8 additions & 2 deletions backend/test/rpc-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import { ClaimEvent } from '../lib/rpc-service/data-types.js'
import { Value } from '@sinclair/typebox/value'

describe('RpcApiClient', () => {
/**
* @param {string} method
* @param {any[]} params
* @returns
*/
const makeRpcRequest = async (method, params) => {
switch (method) {
case 'Filecoin.ChainHead':
return parse(JSON.stringify(chainHeadTestData))
case 'Filecoin.GetActorEventsRaw':
return parse(JSON.stringify(rawActorEventTestData)).filter(e => e.height >= params[0].fromHeight && e.height <= params[0].toHeight)
return parse(JSON.stringify(rawActorEventTestData)).filter((/** @type {{ height: number; }} */ e) => e.height >= params[0].fromHeight && e.height <= params[0].toHeight)
default:
console.error('Unknown method')
}
Expand All @@ -23,7 +28,8 @@ describe('RpcApiClient', () => {
const chainHead = await getChainHead(makeRpcRequest)
assert(chainHead)
const expected = parse(JSON.stringify(chainHeadTestData))
assert.deepStrictEqual(chainHead, expected)
assert(chainHead.Height)
assert.deepStrictEqual(expected.Height, chainHead.Height)
})

for (let blockHeight = 4622129; blockHeight < 4622129 + 11; blockHeight++) {
Expand Down
20 changes: 12 additions & 8 deletions backend/test/spark-api-submit-deals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { after, before, beforeEach, describe, it, mock } from 'node:test'
import { createPgPool, migrateWithPgClient } from '@filecoin-station/deal-observer-db'
import { calculateActiveDealEpochs, daysAgo, daysFromNow, today } from './test-helpers.js'
import { findAndSubmitUnsubmittedDeals } from '../lib/spark-api-submit-deals.js'
/** @import {PgPool} from '@filecoin-station/deal-observer-db' */
/** @import {Queryable} from '@filecoin-station/deal-observer-db' */

describe('Submit deals to spark-api', () => {
/**
* @type {PgPool}
*/
let pgPool

before(async () => {
Expand Down Expand Up @@ -91,6 +96,10 @@ describe('Submit deals to spark-api', () => {
})
})

/**
* @param {Queryable} pgPool
* @param {*} param1
*/
const givenActiveDeal = async (pgPool, { createdAt, startsAt, expiresAt, minerId = 2, clientId = 3, pieceCid = 'cidone', payloadCid = null }) => {
const { activatedAtEpoch, termStart, termMin, termMax } = calculateActiveDealEpochs(createdAt, startsAt, expiresAt)
await pgPool.query(
Expand All @@ -103,12 +112,7 @@ const givenActiveDeal = async (pgPool, { createdAt, startsAt, expiresAt, minerId

// TODO: allow callers of this helper to define how many deals should be reported as skipped
const createSubmitEligibleDealsMock = () => {
return mock.fn(
// original - unused param
() => {},
// implementation
async (deals) => {
return { ingested: deals.length, skipped: 0 }
}
)
return mock.fn(async (deals) => {
return { ingested: deals.length, skipped: 0 }
})
}
4 changes: 2 additions & 2 deletions backend/test/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export const getLocalDayAsISOString = (d) => {

export const today = () => getLocalDayAsISOString(new Date())
export const yesterday = () => getLocalDayAsISOString(new Date(Date.now() - 24 * 60 * 60 * 1000))
export const daysAgo = (n) => getLocalDayAsISOString(new Date(Date.now() - n * 24 * 60 * 60 * 1000))
export const daysFromNow = (n) => getLocalDayAsISOString(new Date(Date.now() + n * 24 * 60 * 60 * 1000))
export const daysAgo = (/** @type {number} */ n) => getLocalDayAsISOString(new Date(Date.now() - n * 24 * 60 * 60 * 1000))
export const daysFromNow = (/** @type {number} */ n) => getLocalDayAsISOString(new Date(Date.now() + n * 24 * 60 * 60 * 1000))

/**
* Calculates activated at, term start, term min, and term max.
Expand Down
Loading