Braid

Async sources

How a source (main or join) can be an array, a promise, a fetcher, or another braid, and exactly what .run() returns in each case.

Both .main() and every .join() accept the same shape of source: an array you already have, or anything that resolves to one.

type BraidSource<TRow> =
	| readonly TRow[]
	| PromiseLike<readonly TRow[]>
	| (() => readonly TRow[] | PromiseLike<readonly TRow[]>);

That’s an array, a promise of one, or a function returning either. Because every Braid instance is itself thenable (PromiseLike), a configured braid also satisfies this type: a stitched result can be handed straight to another braid’s .main() or .join() as its source. This page covers arrays, promises, and fetchers; see the README’s “Composing braids” section and examples/compose.ts for using a braid as a source.

Only a plain array keeps a source synchronous. Everything else (a promise, a fetcher, a braid) has to be awaited, which is what makes .run() return a promise.

What .run() returns

If every source, both the main collection’s and every join’s, is a plain array, .run() runs synchronously and returns the stitched array directly:

const rows = new Braid()
	.main({ source: products, key: (p) => p.sku })
	.join({ name: "bigcommerce", source: bcProducts, on: (bc) => bc.sku, type: "single" })
	.run(); // Row[]

If any source, main or join, isn’t a plain array, .run() returns a Promise of the stitched array instead, even if that source is a function returning a plain array rather than a promise:

const fetched = await new Braid()
	.main({ source: products, key: (p) => p.sku })
	.join({
		name: "bigcommerce",
		source: () => bcProducts, // a plain array, wrapped in a function
		on: (bc) => bc.sku,
		type: "single",
	})
	.run(); // still Promise<Row[]>

A non-array source makes the whole braid async whether or not it ever actually awaits anything: the type is decided by the shape of source (IsAsyncSource<TSource>), not by what it returns at runtime. Once one source anywhere in the chain is non-array, the whole result stays a promise even if every other source is a plain array; nothing later in the chain can undo it.

The builder is thenable

.run() is optional when you’re going to await the result anyway: the builder itself implements .then(), so this works whether or not any source turned out to be async:

const rows = await new Braid()
	.main({ source: products, key: (p) => p.sku })
	.join({ name: "bigcommerce", source: () => fetchBc(), on: (bc) => bc.sku, type: "single" });

This is also exactly what lets a braid be used as another braid’s source: source: someOtherBraid works because someOtherBraid.then exists, not because of anything special about the type being a Braid.

How sources are resolved

A source function is called once per .run(), with no caching or memoization: fetching and caching are the caller’s job, not Braid’s. Wrap the fetcher yourself if you want to reuse a result across calls.

When any source is async, the main collection’s source and every join’s source are all resolved together, through the same Promise.all, before any row is stitched:

const dispatched = await new Braid()
	.main({ name: "order", source: orders, key: (order) => order.reference })
	.join({ name: "shipments", source: fetchShipments, on: (s) => s.orderReference, type: "many" })
	.join({
		name: "customer",
		source: fetchCustomers,
		on: (c) => c.id,
		key: (order) => order.customerId,
		type: "single",
		required: true,
	});

fetchShipments and fetchCustomers are both in flight before either resolves. This costs one round trip, not two. tests/async.test.ts pins the concurrency directly: two joins backed by 40ms fetchers finish in well under 75ms total, which would be impossible if the second fetcher only started once the first had resolved. Plain array sources mixed into the same braid don’t add any artificial delay: they’re used as-is once every async source has resolved.

Rows are only stitched once every source (main and joins, sync and async alike) has resolved. There’s no partial or streaming result; .run() either returns the full array or the full promise.

Errors from an unresolved source

Whatever a source resolves to is still validated as an array. If the main collection’s source resolves to something else, .run()’s promise rejects with a BraidError naming the main collection (or the literal words "main collection" if .main() wasn’t given a name):

The product collection's `source` did not resolve to an array.

A join’s source is checked the same way, naming the join instead:

Join "bigcommerce"'s `source` did not resolve to an array.

If the source itself rejects (a network failure, a thrown error inside an async function, a nested braid’s own required join failing), that rejection propagates through .run()’s promise unchanged. It is not wrapped in a BraidError; catching BraidError specifically won’t catch a rejected fetcher, only Braid’s own validation and required-miss failures. See Errors for the full split between the two.

A required join still throws the same BraidError it would synchronously, just by rejecting the promise instead of throwing immediately. See Defaults and required joins.

The generic-fetcher inference wrinkle

If you build a fetcher with your own generic helper and call it inline (source: fetcherFor(rows), where fetcherFor is a generic function you wrote, not anything Braid exports), TypeScript’s inference can’t see through the call, and the detail type collapses to unknown. Assign the fetcher to a variable first, or pass the type argument explicitly:

function fetcherFor<T>(rows: readonly T[], delayMs: number): () => Promise<readonly T[]> {
	return () => new Promise((resolve) => setTimeout(() => resolve(rows), delayMs));
}

// Detail type collapses to `unknown`:
.join({ name: "bigcommerce", source: fetcherFor(bcProducts, 60), on: (bc) => bc.sku, type: "single" })

// Assign first — inference sees the concrete function type:
const fetchBc = fetcherFor(bcProducts, 60);
.join({ name: "bigcommerce", source: fetchBc, on: (bc) => bc.sku, type: "single" })

Plain arrows (source: () => fetchBc()) and direct function references (source: fetchBc) are unaffected: the wrinkle is specific to calling a generic function and handing its result straight to source in the same expression.

Next steps

  • Type safety covers how TAsync accumulates across sources in the type system.
  • Braid API has the full signature of .run(), .then(), and the BraidSource type.