import { assign, setup } from "xstate"; import { Lifelog, LifelogsResponse, queryLifelogs } from "./lifelogs/query.ts"; import { writeLifelog } from "./lifelogs/write.ts"; import { addDay, isAfter, parse } from "@formkit/tempo"; import { LimitlessPluginSettings } from "../settings.ts"; import { Vault } from "obsidian"; export interface Context { vault: Vault; apiKey: string; latestLog?: Date; lifelogsDir: string; cursor: string | null; lifelogQueue: Lifelog[]; } export type Input = LimitlessPluginSettings & { vault: Vault; }; export type Events = { type: "lifelogs::sync"; }; export const pluginActor = setup({ types: { context: {} as Context, input: {} as Input, events: {} as Events, }, actors: { queryLifelogs, writeLifelog, }, guards: { lifelogQueueNotEmpty: ({ context }) => context.lifelogQueue.length > 0, apiReturnedLifelogs: (_, params: LifelogsResponse) => params.data.lifelogs.length > 0, hasCursor: ({ context }) => Boolean(context.cursor), }, actions: { persistLatestLog: (_, _params: { endDate: Date }) => { // Write back to settings throw new Error("This action must be provided!"); }, updateLatestLog: assign(({ context }, params: { endDate: Date }) => { if (context.latestLog && isAfter(context.latestLog, params.endDate)) { return context; } return { latestLog: params.endDate, }; }), updateCursor: assign((_, params: { cursor: string | null }) => ({ cursor: params.cursor, })), queueLifelogs: assign( ({ context }, params: { lifelogs: LifelogsResponse }) => { return { lifelogQueue: context.lifelogQueue.concat( params.lifelogs.data.lifelogs, ), }; }, ), shiftLifelogQueue: assign(({ context }) => { const newQueue = [...context.lifelogQueue]; newQueue.shift(); return { lifelogQueue: newQueue, }; }), }, }).createMachine({ id: "obsidian-limitless-plugin", initial: "idle", context: ({ input }) => ({ apiKey: input.apiKey, latestLog: input.latestLog ? parse(input.latestLog) : undefined, lifelogsDir: input.lifelogsDir, cursor: null, lifelogQueue: [], vault: input.vault, }), states: { idle: { on: { "lifelogs::sync": { target: "querying", }, }, }, querying: { description: "Pull a page of lifelogs from the Limitless API.", invoke: { src: "queryLifelogs", input: ({ context }) => ({ apiKey: context.apiKey, cursor: context.cursor, start: context.latestLog ?? addDay(new Date(), -7), end: new Date(), }), onDone: [{ target: "writing", guard: { type: "apiReturnedLifelogs", params: ({ event }) => event.output, }, actions: [ { type: "updateCursor", params: ({ event }) => ({ cursor: event.output.meta.lifelogs.nextCursor ?? null, }), }, { type: "queueLifelogs", params: ({ event }) => ({ lifelogs: event.output, }), }, ], }, { target: "idle", }], }, }, writing: { description: "Wait for all logs to be written before requesting the next page.", invoke: { src: "writeLifelog", input: ({ context }) => ({ lifelog: context.lifelogQueue[0], rootDir: context.lifelogsDir, vault: context.vault, }), onDone: { target: "evaluating", actions: [ "shiftLifelogQueue", { type: "updateLatestLog", params: ({ event }) => ({ endDate: event.output, }), }, { type: "persistLatestLog", params: ({ event }) => ({ endDate: event.output, }), }, ], }, }, }, evaluating: { description: "Evaluate next step; either querying, writing, or stopping.", always: [ { target: "writing", guard: "lifelogQueueNotEmpty", }, { target: "querying", guard: "hasCursor", }, "idle", ], }, }, });