···11+<script lang="ts">
22+ import { onDestroy } from 'svelte'
33+ import { register, unregister } from './state'
44+55+ export let date: Date | number
66+ export let locale: string
77+ export let live = true
88+99+ let instance = new Object()
1010+ let text = ''
1111+1212+ register(instance, date, locale, live, value => ({ text } = value))
1313+1414+ onDestroy(() => unregister(instance))
1515+</script>
1616+1717+<span class={$$props.class}>{text}</span>
···11+// keep a cache of formatter per locale, to avoid re-creating them (GC)
22+const formatters = new Map<string, Intl.RelativeTimeFormat>()
33+44+// get the Intl.RelativeTimeFormat formatter for the given locale
55+export function getFormatter(locale: string) {
66+ if (formatters.has(locale)) {
77+ return formatters.get(locale)!
88+ }
99+ const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' })
1010+ formatters.set(locale, formatter)
1111+ return formatter
1212+}
···11+export * from './action'
22+export type { Callback } from './render'
33+export { register, unregister } from './state'
44+export { default as default } from './RelativeTime.svelte'
···11+export type Callback = (result: { seconds: number; count: number; units: Intl.RelativeTimeFormatUnit; text: string }) => void
22+33+export interface RenderState {
44+ date: Date | number
55+ callback: Callback
66+ formatter: Intl.RelativeTimeFormat
77+}
88+99+// Array reprsenting one minute, hour, day, week, month, etc in seconds
1010+const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]
1111+1212+// Array equivalent to the above but in the string representation of the units
1313+const formatUnits: Intl.RelativeTimeFormatUnit[] = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years']
1414+1515+// function to render relative time into
1616+export function render(state: RenderState, now: number) {
1717+ const { date, callback, formatter } = state
1818+1919+ // Allow dates or times to be passed
2020+ const timeMs = typeof date === 'number' ? date : date.getTime()
2121+2222+ // Get the amount of seconds between the given date and now
2323+ const delta = timeMs - now
2424+ const seconds = Math.round(delta / 1000)
2525+2626+ // Grab the ideal cutoff unit
2727+ const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(seconds))
2828+2929+ // units
3030+ const units = formatUnits[unitIndex]
3131+3232+ // Get the divisor to divide from the seconds. E.g. if our unit is 'day' our divisor
3333+ // is one day in seconds, so we can divide our seconds by this to get the # of days
3434+ const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1
3535+3636+ // count of units
3737+ const count = Math.round(seconds / divisor)
3838+3939+ // Intl.RelativeTimeFormat do its magic
4040+ callback({ seconds: seconds, count, units, text: formatter.format(count, units) })
4141+4242+ // calculate time to next update, taking account rounding (e.g. it goes from 2 minutes to 1 minute at 90 seconds)
4343+ // and also the changeover from units (59 seconds shouldn't show as 1 minute, so at 61 seconds we schedule the next
4444+ // update for 1 second time)
4545+4646+ const divisorMs = divisor * 1000
4747+4848+ let updateIn
4949+5050+ if (unitIndex) {
5151+ updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs)
5252+ if (updateIn < 0) {
5353+ updateIn += divisorMs
5454+ }
5555+ } else {
5656+ updateIn = divisorMs - (Math.abs(delta) % divisorMs)
5757+ }
5858+5959+ const updateAt = now + updateIn
6060+6161+ return updateAt
6262+}
···11+import { getFormatter } from './formatter'
22+import { render } from './render'
33+import type { Callback, RenderState } from './render'
44+55+interface UpdateState extends RenderState {
66+ update: number
77+}
88+99+// keep track of each instance
1010+const instances = new Map<Object, UpdateState>()
1111+1212+// we use a single timer for efficiency and to keep updates in sync
1313+let updateInterval: number | NodeJS.Timeout
1414+1515+// register or update instance
1616+export function register(instance: Object, date: Date | number, locale: string, live: boolean, callback: Callback) {
1717+ // get the formatter for the given locale, we do this here so we don't keep having to look it up on each tick
1818+ const formatter = getFormatter(locale)
1919+2020+ // create state to render
2121+ const state = { date, callback, formatter }
2222+2323+ // initial render is immediate, so works for SSR
2424+ const update = render(state, Date.now())
2525+2626+ // if it's to update live, we keep a track and schedule the next update
2727+ if (live) {
2828+ instances.set(instance, { ...state, update })
2929+ } else {
3030+ instances.delete(instance)
3131+ }
3232+3333+ // start the clock ticking if there are any live instances
3434+ if (instances.size) {
3535+ updateInterval =
3636+ updateInterval ||
3737+ setInterval(() => {
3838+ const now = Date.now()
3939+ for (const state of instances.values()) {
4040+ if (state.update <= now) {
4141+ state.update = render(state, now)
4242+ }
4343+ }
4444+ }, 1000)
4545+ }
4646+}
4747+4848+export function unregister(instance: Object) {
4949+ instances.delete(instance)
5050+ if (instances.size === 0) {
5151+ clearInterval(updateInterval)
5252+ updateInterval = 0
5353+ }
5454+}