chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
64
third_party/pi/packages/durable/test/examples/12-tasks.ts
vendored
Normal file
64
third_party/pi/packages/durable/test/examples/12-tasks.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Run a durable task.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/12-tasks.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { createRegistry, defineExtension, defineTask, Harness, MemoryStorage } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
||||
// A task is a small state machine. Its state, the checkpoint, is saved after
|
||||
// every step, so after a crash the next open continues from the last saved
|
||||
// step. The usual pattern: save what you are about to do, do it, then save
|
||||
// the result. A crash between doing and saving reruns that step, so the step
|
||||
// must be safe to repeat; here the fake payment service ignores a repeated key.
|
||||
const payments = new Map<string, number>();
|
||||
type PaymentState = { phase: "prepare" } | { phase: "charge"; key: string };
|
||||
const Payment = defineTask<{ amount: number }, PaymentState, { receipt: number }>({
|
||||
name: "example.payment",
|
||||
version: 1,
|
||||
initial: () => ({ phase: "prepare" }),
|
||||
// One handler per phase. Each must save progress through runtime.commit():
|
||||
// its callback returns the next checkpoint or the final outcome, and that
|
||||
// state is saved in the same commit as everything else the callback wrote.
|
||||
phases: {
|
||||
prepare: async (task, runtime, taskContext) => {
|
||||
await runtime.commit(
|
||||
() => ({ status: "running", checkpoint: { phase: "charge", key: `payment-${task.id}` } }),
|
||||
taskContext,
|
||||
);
|
||||
},
|
||||
charge: async (task, runtime, taskContext) => {
|
||||
const key = task.state.checkpoint.key;
|
||||
if (!payments.has(key)) payments.set(key, task.input.amount * 100);
|
||||
const receipt = payments.get(key)!;
|
||||
await runtime.commit(
|
||||
() => ({ status: "terminal", outcome: { status: "completed", result: { receipt } } }),
|
||||
taskContext,
|
||||
);
|
||||
},
|
||||
},
|
||||
// Runs instead of the phases after harness.abortTask(); it decides the outcome.
|
||||
abort: async (_task, runtime, taskContext) => {
|
||||
await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), taskContext);
|
||||
},
|
||||
});
|
||||
|
||||
// Extensions bring task code; the Harness finds it by name in the registry.
|
||||
// Nothing runs until resume() or a call that waits for progress, such as
|
||||
// waitForTask().
|
||||
const registry = createRegistry();
|
||||
registry.install(defineExtension({ name: "payments", tasks: [Payment] }));
|
||||
const harness = await Harness.open(new MemoryStorage(), { models: createModels(), registry }, context);
|
||||
const root = await harness.root(context);
|
||||
// Every task names its owner. This one belongs to the conversation; a task can
|
||||
// also own child tasks and wait for them (24-child-tasks.ts).
|
||||
const paymentId = await root.commit(
|
||||
(tx) => tx.createTask(Payment, { amount: 5 }, { ownership: { kind: "conversation" } }),
|
||||
context,
|
||||
);
|
||||
// The finished task record is the durable receipt; waitForTask() knows its result type.
|
||||
const paid = await harness.waitForTask(paymentId, context);
|
||||
console.log("payment outcome:", paid.state.outcome);
|
||||
|
||||
await harness.close(context);
|
||||
Reference in New Issue
Block a user