Skip to content
Eugene Lazutkin edited this page Jun 25, 2026 · 1 revision

join

Inner join of two or more sorted streams by key. The inputs are walked in lockstep; at each shared key the equal-key groups are combined into output rows. Streaming and single-pass — only the equal-key groups are held in memory, never a whole input.

import join from 'stream-sorting/sorted/join.js';

// `departments` and `employees`, each already sorted by department id
for await (const row of join({
  dept: {input: departments, key: d => d.id},
  emp: {input: employees, key: e => e.deptId}
})) {
  // row === {dept: {...}, emp: {...}} for every employee with a matching department
}

Inputs are a named map of descriptors. Each descriptor's key extracts the join key (default: the item itself); the map key names the input both on input and in the result. Every input must be sorted by its key under compareKey. The result is an AsyncIterable<R>; convert at the boundary you need:

import readableFrom from 'stream-chain/utils/readableFrom.js';

readableFrom(join(inputs, opts)).pipe(downstream); // Node Readable
ReadableStream.from(join(inputs, opts)).pipeTo(webDestination); // Web ReadableStream

Inputs

Each value in the input map is a descriptor:

Field Type Default Description
input AsyncIterable<T> | Iterable<T> The sorted source stream. Required.
key (item: T) => K identity Extracts the join key from an item.
optional boolean false When true, the input may be absent at a key and is null-filled instead of suppressing the row (an outer join on it).

Options

Option Type Default Description
compareKey (a, b) => number natural order Orders keys (Array.prototype.sort semantics). Provide this or lessKey. Composite keys need an explicit one.
lessKey (a, b) => boolean Strict-less key comparator. Provide this or compareKey.
combine (bag) => R (bag) => bag Builds each output row from the named bag of matched rows. Return undefined to drop that combination.
maxGroupSize number Throw if any single equal-key group exceeds this many items (guards a Cartesian blow-up).

Semantics

  • Inner by default. A row emits only when every required input has the key; keys missing from any required input are skipped.
  • Equal keys form a Cartesian product (SQL semantics): m rows on one side and n on another at the same key emit m × n combinations.
  • combine receives a named bag keyed by the input names — {dept, emp} above — with absent optional inputs as null. The default returns the bag unchanged; returning undefined drops the combination (a join-time filter).
  • N-way. Any number of inputs; the inner join keeps keys present in all required inputs.
  • Outer joins come from optional — mark inputs null-fillable per input, or use leftJoin / fullJoin.
  • Inputs must be sorted by key; an out-of-order input throws at runtime.

Heterogeneous inputs and composite keys

Each input has its own key, so joining differently-shaped tables is the norm — dept keys on d.id, emp keys on e.deptId. Keys meet in a shared comparable space ordered by compareKey. For composite keys, return a tuple and supply a matching comparator:

join(
  {a: {input: a, key: r => [r.deptId, r.empId]}, b: {input: b, key: r => [r.deptId, r.empId]}},
  {compareKey: (x, y) => x[0] - y[0] || x[1] - y[1]}
);

See also

leftJoin, fullJoin, sort, ObjectStreamWrapper.

Clone this wiki locally