obsidian-limitless-plugin/actors/plugin.ts
Graham Barber 985383e6a5
feat: initial version
Working proof-of-concept
2025-03-22 18:09:31 -07:00

162 lines
4.1 KiB
TypeScript

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 const pluginActor = setup({
types: {
context: {} as Context,
input: {} as Input,
},
actors: {
queryLifelogs,
writeLifelog,
},
guards: {
lifelogQueueNotEmpty: ({ context }) => context.lifelogQueue.length > 0,
apiReturnedLifelogs: (_, params: LifelogsResponse) =>
params.data.lifelogs.length > 0,
hasCursor: ({ context }) => Boolean(context.cursor),
},
actions: {
updateLatestLog: assign(({ context }, params: { endDate: Date }) => {
if (context.latestLog && isAfter(context.latestLog, params.endDate)) {
return context;
}
// TODO: Write back to settings
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: "querying",
context: ({ input }) => ({
apiKey: input.apiKey,
latestLog: input.latestLog ? parse(input.latestLog) : undefined,
lifelogsDir: input.lifelogsDir,
cursor: null,
lifelogQueue: [],
vault: input.vault,
}),
states: {
idle: {
after: {
[10 * 60 * 1000]: {
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,
}),
},
],
},
},
},
evaluating: {
description: "Evaluate next step; either querying, writing, or stopping.",
always: [
{
target: "writing",
guard: "lifelogQueueNotEmpty",
},
{
target: "querying",
guard: "hasCursor",
},
"idle",
],
},
},
});