d7002057d7
This PR moves the tldraw.com app into the public repo. ### Change Type - [x] `internal` — Any other changes that don't affect the published package[^2] --------- Co-authored-by: Dan Groshev <git@dgroshev.com> Co-authored-by: alex <alex@dytry.ch>
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import { exhaustiveSwitchError, hasOwnProperty } from '@tldraw/utils'
|
|
|
|
type AlarmOpts = {
|
|
overwrite: 'always' | 'if-sooner'
|
|
}
|
|
|
|
export class AlarmScheduler<Key extends string> {
|
|
storage: () => {
|
|
getAlarm(): Promise<number | null>
|
|
setAlarm(scheduledTime: number | Date): void
|
|
get(key: string): Promise<number | undefined>
|
|
list(options: { prefix: string }): Promise<Map<string, number>>
|
|
delete(keys: string[]): Promise<number>
|
|
put(entries: Record<string, number>): Promise<void>
|
|
}
|
|
alarms: { [K in Key]: () => Promise<void> }
|
|
|
|
constructor(opts: Pick<AlarmScheduler<Key>, 'storage' | 'alarms'>) {
|
|
this.storage = opts.storage
|
|
this.alarms = opts.alarms
|
|
}
|
|
|
|
_alarmsScheduledDuringCurrentOnAlarmCall: Set<Key> | null = null
|
|
async onAlarm() {
|
|
if (this._alarmsScheduledDuringCurrentOnAlarmCall !== null) {
|
|
// i _think_ DOs alarms are one-at-a-time, but throwing here will cause a retry
|
|
throw new Error('onAlarm called before previous call finished')
|
|
}
|
|
this._alarmsScheduledDuringCurrentOnAlarmCall = new Set()
|
|
try {
|
|
const alarms = await this.storage().list({ prefix: 'alarm-' })
|
|
const successfullyExecutedAlarms = new Set<Key>()
|
|
let shouldRetry = false
|
|
let nextAlarmTime = null
|
|
|
|
for (const [key, requestedTime] of alarms) {
|
|
const cleanedKey = key.replace(/^alarm-/, '') as Key
|
|
if (!hasOwnProperty(this.alarms, cleanedKey)) continue
|
|
if (requestedTime > Date.now()) {
|
|
if (nextAlarmTime === null || requestedTime < nextAlarmTime) {
|
|
nextAlarmTime = requestedTime
|
|
}
|
|
continue
|
|
}
|
|
const alarm = this.alarms[cleanedKey]
|
|
try {
|
|
await alarm()
|
|
successfullyExecutedAlarms.add(cleanedKey)
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Error firing alarm ${cleanedKey}:`, err)
|
|
shouldRetry = true
|
|
}
|
|
}
|
|
|
|
const keysToDelete = []
|
|
for (const key of successfullyExecutedAlarms) {
|
|
if (this._alarmsScheduledDuringCurrentOnAlarmCall.has(key)) continue
|
|
keysToDelete.push(`alarm-${key}`)
|
|
}
|
|
if (keysToDelete.length > 0) {
|
|
await this.storage().delete(keysToDelete)
|
|
}
|
|
|
|
if (shouldRetry) {
|
|
throw new Error('Some alarms failed to fire, scheduling retry')
|
|
} else if (nextAlarmTime !== null) {
|
|
await this.setCoreAlarmIfNeeded(nextAlarmTime)
|
|
}
|
|
} finally {
|
|
this._alarmsScheduledDuringCurrentOnAlarmCall = null
|
|
}
|
|
}
|
|
|
|
private async setCoreAlarmIfNeeded(targetAlarmTime: number) {
|
|
const currentAlarmTime = await this.storage().getAlarm()
|
|
if (currentAlarmTime === null || targetAlarmTime < currentAlarmTime) {
|
|
await this.storage().setAlarm(targetAlarmTime)
|
|
}
|
|
}
|
|
|
|
async scheduleAlarmAt(key: Key, time: number | Date, opts: AlarmOpts) {
|
|
const targetTime = typeof time === 'number' ? time : time.getTime()
|
|
if (this._alarmsScheduledDuringCurrentOnAlarmCall !== null) {
|
|
this._alarmsScheduledDuringCurrentOnAlarmCall.add(key)
|
|
}
|
|
switch (opts.overwrite) {
|
|
case 'always':
|
|
await this.storage().put({ [`alarm-${key}`]: targetTime })
|
|
break
|
|
case 'if-sooner': {
|
|
const currentScheduled = await this.storage().get(`alarm-${key}`)
|
|
if (!currentScheduled || currentScheduled > targetTime) {
|
|
await this.storage().put({ [`alarm-${key}`]: targetTime })
|
|
}
|
|
break
|
|
}
|
|
default:
|
|
exhaustiveSwitchError(opts.overwrite)
|
|
}
|
|
await this.setCoreAlarmIfNeeded(targetTime)
|
|
}
|
|
|
|
async scheduleAlarmAfter(key: Key, delayMs: number, opts: AlarmOpts) {
|
|
await this.scheduleAlarmAt(key, Date.now() + delayMs, opts)
|
|
}
|
|
|
|
async getAlarm(key: Key): Promise<number | null> {
|
|
return (await this.storage().get(`alarm-${key}`)) ?? null
|
|
}
|
|
|
|
async deleteAlarm(key: Key): Promise<void> {
|
|
await this.storage().delete([`alarm-${key}`])
|
|
}
|
|
}
|