import { assert, it } from "@effect/vitest";
import { Effect, Option } from "effect";
import { CompletionObserver, completedInActiveList } from "./completion.ts";
import { TodoItem, TodoList, TrackerState } from "../core/domain.ts";
interface ItemSpec {
readonly id: number;
readonly done: boolean;
readonly deps?: readonly string[];
}
const list = (id: number, name: string, items: readonly ItemSpec[]): TodoList =>
new TodoList({
id,
name,
items: items.map(
(item) =>
new TodoItem({
id: item.id,
title: `item ${item.id}`,
done: item.done,
deps: [...(item.deps ?? [])],
}),
),
nextItemId: items.reduce((max, item) => Math.max(max, item.id), 0) + 1,
});
const state = (
lists: readonly TodoList[],
activeListId: number | null,
nextListId = lists.length + 1,
): TrackerState => new TrackerState({ lists: [...lists], activeListId, nextListId });
// ---------------------------------------------------------------------------
// completedInActiveList
// ---------------------------------------------------------------------------
it("completedInActiveList returns every item that transitioned from open to done, in list order", () => {
const before = state(
[
list(1, "Work", [
{ id: 1, done: false },
{ id: 3, done: false },
]),
],
1,
);
const after = state(
[
list(1, "Work", [
{ id: 1, done: true },
{ id: 3, done: true },
]),
],
1,
);
assert.deepStrictEqual(completedInActiveList(before, after), [1, 3]);
});
it("completedInActiveList returns a single id for a single completion", () => {
const before = state([list(1, "Work", [{ id: 1, done: false }])], 1);
const after = state([list(1, "Work", [{ id: 1, done: true }])], 1);
assert.deepStrictEqual(completedInActiveList(before, after), [1]);
});
it("completedInActiveList ignores an item that was already done", () => {
const before = state([list(1, "Work", [{ id: 1, done: true }])], 1);
const after = state([list(1, "Work", [{ id: 1, done: true }])], 1);
assert.deepStrictEqual(completedInActiveList(before, after), []);
});
it("completedInActiveList ignores a completion in a list that is not active", () => {
const before = state(
[list(1, "Work", [{ id: 1, done: false }]), list(2, "Other", [{ id: 1, done: false }])],
1,
);
const after = state(
[list(1, "Work", [{ id: 1, done: false }]), list(2, "Other", [{ id: 1, done: true }])],
1,
);
assert.deepStrictEqual(completedInActiveList(before, after), []);
});
// ---------------------------------------------------------------------------
// CompletionObserver
// ---------------------------------------------------------------------------
const withObserver = (
program: Effect.Effect,
): Effect.Effect => program.pipe(Effect.provide(CompletionObserver.layer));
it.effect("CompletionObserver record folds successive mutations into one batch", () =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state(
[
list(1, "Work", [
{ id: 1, done: false },
{ id: 3, done: false },
]),
],
1,
);
const s1 = state(
[
list(1, "Work", [
{ id: 1, done: true },
{ id: 3, done: false },
]),
],
1,
);
const s2 = state(
[
list(1, "Work", [
{ id: 1, done: true },
{ id: 3, done: true },
]),
],
1,
);
yield* observer.record(s0, s1);
yield* observer.record(s1, s2);
const batch = Option.getOrThrow(yield* observer.consume);
assert.deepStrictEqual(batch.completedItemIds, [1, 3]);
assert.deepStrictEqual(
batch.list.items.map((item) => item.done),
[true, true],
);
}),
),
);
it.effect(
"CompletionObserver record refreshes the snapshot when a later mutation completes nothing",
() =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state([list(1, "Work", [{ id: 1, done: false }])], 1);
const s1 = state([list(1, "Work", [{ id: 1, done: true }])], 1);
const s2 = state(
[
list(1, "Work", [
{ id: 1, done: true },
{ id: 2, done: false },
]),
],
1,
);
yield* observer.record(s0, s1);
yield* observer.record(s1, s2);
const batch = Option.getOrThrow(yield* observer.consume);
assert.deepStrictEqual(batch.completedItemIds, [1]);
assert.deepStrictEqual(
batch.list.items.map((item) => item.id),
[1, 2],
);
}),
),
);
it.effect("CompletionObserver record drops a completed item that was reopened before settle", () =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state([list(1, "Work", [{ id: 1, done: false }])], 1);
const s1 = state([list(1, "Work", [{ id: 1, done: true }])], 1);
const s2 = state([list(1, "Work", [{ id: 1, done: false }])], 1);
yield* observer.record(s0, s1);
yield* observer.record(s1, s2);
assert.strictEqual(Option.isNone(yield* observer.consume), true);
}),
),
);
it.effect("CompletionObserver record drops a completed item that was removed before settle", () =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state([list(1, "Work", [{ id: 1, done: false }])], 1);
const s1 = state([list(1, "Work", [{ id: 1, done: true }])], 1);
const s2 = state([list(1, "Work", [])], 1);
yield* observer.record(s0, s1);
yield* observer.record(s1, s2);
assert.strictEqual(Option.isNone(yield* observer.consume), true);
}),
),
);
it.effect(
"CompletionObserver record clears the batch when the active list changes without a completion",
() =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state(
[list(1, "Work", [{ id: 1, done: false }]), list(2, "Other", [{ id: 1, done: false }])],
1,
);
const s1 = state(
[list(1, "Work", [{ id: 1, done: true }]), list(2, "Other", [{ id: 1, done: false }])],
1,
);
const s2 = state(
[list(1, "Work", [{ id: 1, done: true }]), list(2, "Other", [{ id: 1, done: false }])],
2,
);
yield* observer.record(s0, s1);
yield* observer.record(s1, s2);
assert.strictEqual(Option.isNone(yield* observer.consume), true);
}),
),
);
it.effect(
"CompletionObserver record starts a fresh batch when a completion arrives in a different active list",
() =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state(
[list(1, "Work", [{ id: 1, done: false }]), list(2, "Other", [{ id: 1, done: false }])],
1,
);
const s1 = state(
[list(1, "Work", [{ id: 1, done: true }]), list(2, "Other", [{ id: 1, done: false }])],
1,
);
const s2 = state(
[list(1, "Work", [{ id: 1, done: true }]), list(2, "Other", [{ id: 1, done: true }])],
2,
);
yield* observer.record(s0, s1);
yield* observer.record(s1, s2);
const batch = Option.getOrThrow(yield* observer.consume);
assert.deepStrictEqual(batch.completedItemIds, [1]);
assert.strictEqual(batch.list.name, "Other");
}),
),
);
it.effect("CompletionObserver consume clears the record", () =>
withObserver(
Effect.gen(function* () {
const observer = yield* CompletionObserver;
const s0 = state([list(1, "Work", [{ id: 1, done: false }])], 1);
const s1 = state([list(1, "Work", [{ id: 1, done: true }])], 1);
yield* observer.record(s0, s1);
const first = yield* observer.consume;
const second = yield* observer.consume;
assert.strictEqual(Option.isSome(first), true);
assert.strictEqual(Option.isNone(second), true);
}),
),
);