Name the two values the create input will need: committed todos and the text being typed. Add these contracts in src/application.ts.
Name the application state
export class TodoList extends RefSubject.Service<TodoList, Domain.TodoList>()("TodoList") {}
export class TodoText extends RefSubject.Service<TodoText, string>()("TodoText") {}
TodoList and TodoText are service declarations: actions request these names, and a Layer supplies their values when the application runs. Declaring them does not allocate a global list or input. The same action can use empty state in the browser or prepared state in a test. See shared state contracts for requesting and providing a state service.
Keep the values separate: submitting text changes the list, but typing does not. Counts and filtered rows will be derived from that list later.
Ask for a new Todo
export class CreateTodo extends Context.Service<
CreateTodo,
(text: string) => Effect.Effect<Domain.Todo>
>()("CreateTodo") {
static readonly call = (text: string) =>
Effect.flatMap(CreateTodo, (createTodo) => createTodo(text));
}
Creation needs an ID and a timestamp. CreateTodo lets the application ask for a complete Todo without choosing how those values are produced. Infrastructure can use browser crypto and a clock; a test can return predictable values.
The return type says creation is an Effect. Merely requesting the function does not run it. The next chapter will call it after checking the draft.
Predict: if two independent applications provide different TodoList layers, do they share one global array?
Check your prediction
No. Each provided state instance owns its own value. Shared state contracts explains how to deliberately share one provider instead.
Complete files
Keep the files from the previous step and replace or add these. Each full file is the source used by this milestone; the excerpts above select lines from it.
src/domain.ts
import * as Schema from "effect/Schema";
export const TodoId = Schema.String.pipe(Schema.brand("TodoId"));
export type TodoId = typeof TodoId.Type;
export const Todo = Schema.Struct({
id: TodoId,
text: Schema.String,
completed: Schema.Boolean,
timestamp: Schema.DateTimeUtcFromString,
});
export type TodoJson = typeof Todo.Encoded;
export type Todo = typeof Todo.Type;
export const TodoList = Schema.Array(Todo);
export type TodoListJson = typeof TodoList.Encoded;
export type TodoList = typeof TodoList.Type;
export const FilterState = Schema.Literals(["all", "active", "completed"]);
export type FilterState = typeof FilterState.Type;
export function updateTodo(list: TodoList, id: TodoId, f: (todo: Todo) => Todo): TodoList {
return list.map((todo) => (todo.id === id ? f(todo) : todo));
}
export function editText(id: TodoId, text: string) {
return (list: TodoList): TodoList => updateTodo(list, id, (todo) => ({ ...todo, text }));
}
export function toggleCompleted(id: TodoId) {
return (list: TodoList): TodoList =>
updateTodo(list, id, (todo) => ({ ...todo, completed: !todo.completed }));
}
export function isCompleted(todo: Todo): boolean {
return todo.completed;
}
export function isActive(todo: Todo): boolean {
return !todo.completed;
}
export function toggleAllCompleted(list: TodoList): TodoList {
if (list.some(isActive)) {
return list.map((todo) => ({ ...todo, completed: true }));
} else {
return list.map((todo) => ({ ...todo, completed: false }));
}
}
export function deleteTodo(id: TodoId) {
return (list: TodoList): TodoList => list.filter((todo) => todo.id !== id);
}
export function clearCompleted(list: TodoList): TodoList {
return list.filter(isActive);
}
export function activeCount(list: TodoList): number {
return list.filter(isActive).length;
}
export function completedCount(list: TodoList): number {
return list.filter(isCompleted).length;
}
export function allAreCompleted(list: TodoList): boolean {
return list.length > 0 && list.every(isCompleted);
}
export function someAreCompleted(list: TodoList): boolean {
return list.some(isCompleted);
}
export function filterTodoList({ list, state }: { list: TodoList; state: FilterState }): TodoList {
switch (state) {
case "all":
return list;
case "active":
return list.filter(isActive);
case "completed":
return list.filter(isCompleted);
}
}
export function updateText(text: string) {
return (todo: Todo): Todo => ({ ...todo, text });
}
src/application.ts
import * as Effect from "effect/Effect";
import * as Context from "effect/Context";
import { RefSubject } from "@typed/fx";
import * as Domain from "./domain.js";
export class CreateTodo extends Context.Service<
CreateTodo,
(text: string) => Effect.Effect<Domain.Todo>
>()("CreateTodo") {
static readonly call = (text: string) =>
Effect.flatMap(CreateTodo, (createTodo) => createTodo(text));
}
export class TodoList extends RefSubject.Service<TodoList, Domain.TodoList>()("TodoList") {}
export class TodoText extends RefSubject.Service<TodoText, string>()("TodoText") {}