Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ The navigation validation tests run with `pnpm test`. They verify tool roots, de

## Interactive Throughline examples

Selected Throughline guides render the published `@0xbuff/throughline@0.0.2` React player. Sources are kept in `src/lib/throughline-demos.ts`; MDX pages use `<ThroughlineDemo id="..." />`. The component is registered in `src/components/mdx.tsx`. Its compiler, player and namespaced CSS load in a separate module only when a demo is rendered. All other 0xBUFF tool documentation remains independent of Throughline.
Selected Throughline guides render the published `@0xbuff/throughline@0.0.2` React player. The overview leads with a multi-system checkout recovery trace; introductory pages retain smaller teaching examples. Sources are kept in `src/lib/throughline-demos.ts`; MDX pages use `<ThroughlineDemo id="..." />`. The component is registered in `src/components/mdx.tsx`. Its compiler, player and namespaced CSS load in a separate module only when a demo is rendered. All other 0xBUFF tool documentation remains independent of Throughline.

The showcase is deliberately limited to a few pages and does not automatically transform every `thl` code fence. This preserves portable source examples and avoids unnecessary client JavaScript. `scripts/throughline-interactive.test.mjs` compiles every showcased trace with the installed package; CI also typechecks and builds both environments.
The showcase is deliberately limited to a few pages and does not automatically transform every `thl` code fence. The official `remarkThroughline({ mode: "mdx" })` integration and `ThroughlineEmbed` require a subsequent package release; v0.0.2 must not be assumed to contain them. This preserves portable source examples and avoids unnecessary client JavaScript. `scripts/throughline-interactive.test.mjs` compiles every showcased trace with the installed package; CI also typechecks and builds both environments.

## Content ownership and publication

Expand Down
4 changes: 2 additions & 2 deletions content/docs/throughline/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Use it to explain algorithms, data structures, recursion, and system workflows.

## See it in action

<ThroughlineDemo id="counter" />
<ThroughlineDemo id="checkout" />

Open **View trace source** to see exactly what the player is visualizing. The compiler creates the frames before playback; **Play** and the timeline only select existing frames.
This checkout scenario follows a lost provider reply, an unknown payment outcome, a retry with the same idempotency key and eventual recovery. Directed transfers show what was **sent**; order, provider and customer values change only at explicitly authored writes. Open **View trace source** to inspect both kinds of events. The compiler creates the frames before playback; **Play** and the timeline only select existing frames.

## Get started

Expand Down
43 changes: 42 additions & 1 deletion scripts/throughline-interactive.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { compile } from "@0xbuff/throughline";
import { THROUGHLINE_DEMOS } from "../src/lib/throughline-demos.ts";

test("interactive documentation demos compile with the published Throughline package", () => {
assert.deepEqual(Object.keys(THROUGHLINE_DEMOS).sort(), ["binary", "counter", "references"]);
assert.deepEqual(Object.keys(THROUGHLINE_DEMOS).sort(), ["binary", "checkout", "counter", "references"]);
for (const [id, demo] of Object.entries(THROUGHLINE_DEMOS)) {
const trace = compile(demo.source);
assert.equal(trace.version, 1, id);
Expand All @@ -14,6 +14,47 @@ test("interactive documentation demos compile with the published Throughline pac
}
});

test("the checkout demo preserves an unknown outcome until confirmation", () => {
const trace = compile(THROUGHLINE_DEMOS.checkout.source);
const get = (index, id) => trace.frames[index]?.state.entities.find((item) => item.id === id)?.value;
assert.deepEqual(get(6, "order"), { status: "awaiting_payment" });
assert.equal(get(6, "worker"), "outcome unknown");
assert.equal(get(6, "status"), "processing");
assert.equal(get(8, "provider"), "one charge: pay-order-1; retry deduplicated");
assert.deepEqual(get(11, "order"), { status: "paid" });
assert.equal(get(11, "status"), "confirmed");
});

test("checkout transfers preserve real link endpoints and never imply persistence", () => {
const trace = compile(THROUGHLINE_DEMOS.checkout.source);
const links = new Map(trace.frames[0].state.relations.map((link) => [link.id, link]));
const expected = [
["persist"], ["enqueue"], ["report"], ["deliver"], ["charge"], [],
["retry"], ["charge"], ["receipt"], ["confirm", "persist"], ["report"],
];
for (const [index, relationIds] of expected.entries()) {
const frame = trace.frames[index + 1];
assert.deepEqual(frame.state.transfers.map(({ relation }) => relation), relationIds, `Step ${index + 1}`);
for (const transfer of frame.state.transfers) {
const link = links.get(transfer.relation);
assert.ok(link, `Unknown link ${transfer.relation}`);
assert.ok(frame.state.entities.some((entity) => entity.id === link.from));
assert.ok(frame.state.entities.some((entity) => entity.id === link.to));
}
}

const state = (index, id) =>
trace.frames[index].state.entities.find((entity) => entity.id === id)?.value;
assert.equal(state(4, "provider"), "no charge", "delivery is not a persisted provider charge");
assert.deepEqual(state(9, "order"), { status: "awaiting_payment" }, "confirmation is not an order write");
assert.equal(state(10, "status"), "processing", "recording paid does not automatically notify the customer");
assert.equal(state(11, "status"), "confirmed");
assert.deepEqual(
trace.frames[10].state.transfers.find(({ relation }) => relation === "persist")?.value,
{ status: "paid" },
);
});

test("the counter and binary demos show their expected final values", () => {
const counter = compile(THROUGHLINE_DEMOS.counter.source);
const lastCounter = counter.frames.at(-1)?.state.entities.find((item) => item.id === "counter");
Expand Down
72 changes: 72 additions & 0 deletions src/lib/throughline-demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,78 @@ export const THROUGHLINE_DEMOS = {
].join("\n"),
expectedFrames: 3,
},
checkout: {
title: "Recover a distributed checkout after a timeout",
description: "Trace a payment whose reply is lost, then follow an idempotent retry through order recovery.",
source: [
"thl flow",
'group "Checkout" as column',
'group "Payment" as column',
'node api "Checkout API" in "Checkout" = "received"',
'node order "Order record" in "Checkout" as record = {"status":"absent"}',
'node status "Customer status" in "Checkout" = "not started"',
'node queue "Payment queue" in "Payment" as array = []',
'node worker "Worker" in "Payment" = "idle"',
'node provider "Provider" in "Payment" = "no charge"',
'link persist api -> order "persist"',
'link enqueue api -> queue "enqueue"',
'link deliver queue -> worker "deliver"',
'link charge worker -> provider "charge"',
'link report api -> status "status"',
'link retry worker -> queue "retry"',
'link receipt provider -> worker "confirmation"',
'link confirm worker -> api "confirm payment"',
'step "Persist pending order"',
' send persist = {"status":"awaiting_payment"}',
' set order = {"status":"awaiting_payment"}',
' note "The order is pending. No payment is confirmed yet."',
'step "Enqueue payment work"',
' send enqueue = "pay-order-1"',
' set queue = ["pay-order-1"]',
' note "A job is queued with a stable payment key."',
'step "Report processing"',
' send report = "processing"',
' set status = "processing"',
' note "The customer is told the operation is in progress, not that it succeeded."',
'step "Deliver the job"',
' send deliver = "pay-order-1"',
' set queue = []',
' set worker = "charging with pay-order-1"',
' note "The worker begins the first payment attempt."',
'step "Provider records one charge"',
' send charge = "pay-order-1"',
' set provider = "one charge: pay-order-1; reply lost"',
' note "The provider recorded one charge, but its response was lost."',
'step "Worker times out"',
' set worker = "outcome unknown"',
' note "A timeout does not establish payment failure. The order remains pending."',
'step "Retry with the same key"',
' send retry = "pay-order-1"',
' set queue = ["retry pay-order-1"]',
' set worker = "retry queued"',
' note "The retry uses the original idempotency key."',
'step "Provider deduplicates"',
' send charge = "pay-order-1"',
' set queue = []',
' set provider = "one charge: pay-order-1; retry deduplicated"',
' set worker = "retrying pay-order-1"',
' note "For this provider, the same key identifies the existing charge."',
'step "Worker receives confirmation"',
' send receipt = "pay-order-1 paid"',
' set worker = "payment confirmed"',
' note "The response arrives before local payment success is recorded."',
'step "Persist paid order"',
' send confirm = "pay-order-1 paid"',
' send persist = {"status":"paid"}',
' set order = {"status":"paid"}',
' note "The confirmed outcome is now persisted."',
'step "Notify customer"',
' send report = "confirmed"',
' set status = "confirmed"',
' note "The customer sees the recovered order; no second charge was created."',
].join("\n"),
expectedFrames: 12,
},
references: {
title: "Watch two references share an array",
description: "Step through copying a reference, mutating the array, and moving one pointer.",
Expand Down
Loading