-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Re-remind-me: New reminder parser/scheduler (#231)
The new parser is custom-built and relies on extracting written patterns from the text instead of on cron patterns. The scheduler consumes those patterns and handles scheduling job execution. The scheduler component maintains a sorted list of next job executions, and schedules the next run based on the first item in that list. An initial list of parsing and scheduler tests is included. Still missing: formatting tests! May hold on those for now. Fixes #139, fixes #45.
- Loading branch information
Showing
12 changed files
with
2,937 additions
and
234 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import('ts-jest').JestConfigWithTsJest} */ | ||
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
export type JobMessageInfo = { | ||
userId: string | ||
message: string | ||
room: string | ||
threadId?: string | ||
} | ||
|
||
export type BaseJobSpec<Type extends string, SpecType> = { | ||
type: Type | ||
spec: SpecType | ||
} | ||
|
||
export type BaseJobDefinition<Type extends string, SpecType> = BaseJobSpec< | ||
Type, | ||
SpecType | ||
> & { | ||
messageInfo: JobMessageInfo | ||
} | ||
|
||
export type BaseJob<Type extends string, SpecType> = BaseJobDefinition< | ||
Type, | ||
SpecType | ||
> & { | ||
next: string | ||
} | ||
|
||
export type JobSpec = | ||
| BaseJobSpec<"single", SingleShotDefinition> | ||
| BaseJobSpec<"recurring", RecurringDefinition> | ||
|
||
export type JobDefinition = | ||
| BaseJobDefinition<"single", SingleShotDefinition> | ||
| BaseJobDefinition<"recurring", RecurringDefinition> | ||
|
||
export type SingleJob = BaseJob<"single", SingleShotDefinition> | ||
export type RecurringJob = BaseJob<"recurring", RecurringDefinition> | ||
|
||
export type Job = SingleJob | RecurringJob | ||
|
||
export type PersistedJob = | ||
| (SingleJob & { id: number }) | ||
| (RecurringJob & { id: number }) | ||
|
||
export type SingleShotDefinition = { | ||
hour: number | ||
minute: number | ||
dayOfWeek: number | ||
} | ||
|
||
export type RecurringDefinition = | ||
| (SingleShotDefinition & { | ||
repeat: "week" | ||
interval: number | ||
}) | ||
| { | ||
hour: number | ||
minute: number | ||
repeat: "month" | ||
dayOfMonth: number | ||
} | ||
|
||
export type RecurrenceSpec = SingleShotDefinition | RecurringDefinition |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as dayjs from "dayjs" | ||
import * as utc from "dayjs/plugin/utc" | ||
import * as timezone from "dayjs/plugin/timezone" | ||
import * as localizedFormat from "dayjs/plugin/localizedFormat" | ||
import * as advancedFormat from "dayjs/plugin/advancedFormat" | ||
|
||
import { encodeThreadId, matrixUrlFor } from "../adapter-util" | ||
import { PersistedJob, RecurringDefinition } from "./data" | ||
|
||
dayjs.extend(utc) | ||
dayjs.extend(timezone) | ||
dayjs.extend(localizedFormat) | ||
dayjs.extend(advancedFormat) | ||
|
||
// List of escape regex patterns (search to replace) to use when formatting a | ||
// reminder message for display. Used to show raw input and avoid tagging | ||
// people when simply trying to explore the reminder list. | ||
const formattingEscapes = { | ||
"(@@?)": "[$1]", | ||
"```": "\n```\n", | ||
"#": "[#]", | ||
"\n": "\n>", | ||
} | ||
|
||
function formatNextOccurrence(nextIsoRecurrenceDate: string): string { | ||
return dayjs(nextIsoRecurrenceDate).format("llll z") | ||
} | ||
|
||
function formatRecurringSpec( | ||
spec: RecurringDefinition, | ||
nextOccurrence: string, | ||
): string { | ||
const formattedNextOccurrence = formatNextOccurrence(nextOccurrence) | ||
|
||
if (spec.repeat === "week") { | ||
const baseDate = dayjs() | ||
.day(spec.dayOfWeek) | ||
.hour(spec.hour) | ||
.minute(spec.minute) | ||
|
||
return ( | ||
formattedNextOccurrence + | ||
baseDate.format("[ (recurs weekly on] dddd [at] HH:mm[)]") | ||
) | ||
} | ||
|
||
const baseDate = dayjs() | ||
.date(spec.dayOfMonth) | ||
.hour(spec.hour) | ||
.minute(spec.minute) | ||
|
||
return ( | ||
formattedNextOccurrence + | ||
baseDate.format("[ (recurs monthly on the] Do [at] HH:mm[)]") | ||
) | ||
} | ||
|
||
export function formatJobForMessage(job: PersistedJob): string { | ||
const { message: jobMessage, room, threadId } = job.messageInfo | ||
|
||
const formattedSpec = | ||
job.type === "single" | ||
? formatNextOccurrence(job.next) | ||
: formatRecurringSpec(job.spec, job.next) | ||
|
||
// FIXME Resolve an actual display name here? Or let the adpater feed it to us? | ||
const jobRoomDisplayName = room | ||
|
||
const targetDisplayText = | ||
threadId === undefined | ||
? `(to ${jobRoomDisplayName})` | ||
: `(to [thread in ${jobRoomDisplayName}](${matrixUrlFor( | ||
room, | ||
"thesis.co", | ||
encodeThreadId(threadId), | ||
)}))` | ||
|
||
const messageParsed = Object.entries(formattingEscapes).reduce( | ||
(formattedMessage, [pattern, replacement]) => | ||
formattedMessage.replace(new RegExp(pattern, "g"), replacement), | ||
jobMessage, | ||
) | ||
|
||
return `ID ${job.id}: **${formattedSpec}** ${targetDisplayText}:\n>${messageParsed}\n\n` | ||
} | ||
|
||
export function formatJobsForListMessage(jobs: PersistedJob[]) { | ||
return jobs.map((job) => formatJobForMessage(job)).join("\n\n") | ||
} |
Oops, something went wrong.