1. Parse one update
Implement parseEntity(value: unknown): ProductUpdate. Throw an Error on invalid input; invalid input does not return null or undefined. Require own nonempty sourceId, eventId and productId strings, normalize them with trim, require a nonnegative safe integer version and finite nonnegative numeric price. Reject null, arrays, numeric strings and missing fields. Return a new object; unrelated fields are ignored. The numeric field represents the selected domain price, not a claim that every real business price follows this simplified contract.
Spoiler
Exclude null and arrays first, then establish each field type and business condition. Validate syntax before conversion when a different contract allows strings. The local Record assertion below is restricted to an already-checked object; each field is still checked at runtime. It is not an assertion that the whole input is valid.
export type ProductUpdate = {
sourceId: string;
eventId: string;
productId: string;
version: number;
price: number;
};
export function parseEntity(value: unknown): ProductUpdate {
if (typeof value !== 'object' || value === null || Array.isArray(value))
throw new Error('Expected object');
const object = value;
function text(key: string): string {
if (!Object.hasOwn(object, key)) throw new Error(`Missing ${key}`);
const field = (object as Record<string, unknown>)[key];
if (typeof field !== 'string' || !field.trim()) throw new Error(`Invalid ${key}`);
return field.trim();
}
if (
!('version' in value) ||
typeof value.version !== 'number' ||
!Number.isSafeInteger(value.version) ||
value.version < 0
)
throw new Error('Invalid version');
if (
!('price' in value) ||
typeof value.price !== 'number' ||
!Number.isFinite(value.price) ||
value.price < 0
)
throw new Error('Invalid price');
return {
sourceId: text('sourceId'),
eventId: text('eventId'),
productId: text('productId'),
version: value.version,
price: value.price,
};
}
2. Deduplicate and keep the latest
Input is already valid. eventId is unique within sourceId; immutable duplicate deliveries count once. Select the highest version per sourceId/productId, keep the first at equal version and preserve first entity appearance. Do not mutate input. Check one ID in two sources, reversed versions, duplicate events and separator characters in IDs.
Spoiler
Use two independent keys. A Set records delivered events and a Map keeps entity winners. JSON encoding a pair avoids delimiter collisions. Replace only for a greater version; Map preserves the position of an existing key. This batch-only deduplication is not a durable inbox and retains O(events + entities) keys.
export type ProductUpdate = {
sourceId: string;
eventId: string;
productId: string;
version: number;
price: number;
};
export function latestEntities(input: readonly ProductUpdate[]): ProductUpdate[] {
const seen = new Set<string>();
const latest = new Map<string, ProductUpdate>();
for (const entity of input) {
const eventKey = JSON.stringify([entity.sourceId, entity.eventId]);
if (seen.has(eventKey)) continue;
seen.add(eventKey);
const entityKey = JSON.stringify([entity.sourceId, entity.productId]);
const current = latest.get(entityKey);
if (!current || entity.version > current.version) latest.set(entityKey, entity);
}
return [...latest.values()];
}
3. Process with partial failures
Accept already-validated entities, a positive safe integer limit and save(entity): Promise<void>. Bound active saves, preserve input result order, keep running after per-entity failures, and return an ok union for each entity. An invalid limit rejects even when input is empty. A synchronous throw and throw undefined are failures too.
Spoiler
Catch inside the limiter callback so expected failures become values. Await save before reporting success. Keep Error narrowing explicit. mapLimit is the helper from chapter 5 and examples/core.ts; it is imported here rather than duplicated. Type the union result to preserve literal discriminators.
import { mapLimit } from './core.ts';
export type ProductUpdate = {
sourceId: string;
eventId: string;
productId: string;
version: number;
price: number;
};
export type Outcome = { ok: true; entity: ProductUpdate } | { ok: false; error: string };
export async function processEntities(
input: readonly ProductUpdate[],
limit: number,
save: (entity: ProductUpdate) => Promise<void>,
): Promise<Outcome[]> {
if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError('Invalid limit');
return mapLimit<ProductUpdate, Outcome>(input, limit, async (entity) => {
try {
await save(entity);
return { ok: true, entity };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
});
}
Check your solution
Write and run your implementation in your usual TypeScript editor. Work through the checks below, then open each spoiler and compare the reasoning. The Copy code button copies a displayed example; it does not run or check your solution.
Parser checks
- Valid input with surrounding spaces in the IDs and
price: 0→ trimmed IDs, zero preserved and a new object. Extra fields are ignored. null, an array, a missing field, a blank ID, a negative or fractional version, a numeric string,NaNorInfinity→ throw anError.- A required string inherited from the prototype → reject it; the contract requires own fields.
Deduplication checks
- One entity at versions
3, 1, 3→ the first version-3 record wins. - Repeat the same
sourceId/eventId→ one delivery counts. The same entity ID in two sources → two separate winners. - IDs containing separators → remain distinct. Result order follows first entity appearance; input is unchanged.
Async checks
- Saves finish in reverse order → results still follow input order; peak active saves never exceeds
limit. - One save rejects or throws
undefined→ that position hasok: false; later items still run. A save must finish before its result hasok: true. - Empty input with a valid limit →
[]. An invalid limit, including with empty input → reject withRangeError.
Run your editor’s type checker and tests against your implementation. Finally, change one condition, such as the tie-breaking or failure policy, close the answers and implement the variation yourself.