Queries
world.query returns a generator-backed view over the underlying stores. Three ways to consume it:
const q = w.query([pos_c, vel_c] as const);
// 1. for...of (live iterator)for (const [id, p, v] of q) { p.x += v.dx; // safe: in-place updates of existing components}
// 2. .each callbackq.each((id, p, v) => { p.x += v.dx; });
// 3. .collect() snapshotfor (const [id, p, v] of q.collect()) { if (p.x > 100) w.despawn(id); // safe — the array was captured first}Marker auto-elision
Section titled “Marker auto-elision”Component<true> markers (e.g. player_c: Component<true>) are filter-only — they restrict which entities match but never appear in the yielded tuple. The destructure shape is [Id, ...data]:
// player_c is a marker — it filters but is elided from the tuplefor (const [id, p, d] of w.query([pos_c, player_c, dir_c] as const).collect()) { // id: Id, p: Pos, d: Dir — no `true` slot to skip p.x += d.dx;}
// pure-marker query — only `Id` in the tuplefor (const [id] of w.query([player_c] as const)) { /* … */ }The as const is required for TypeScript to keep the component tuple ordered. Marker-only / data-only / mixed queries all share the same iterator semantics.
Mutation safety
Section titled “Mutation safety”set-ting the same entity in a component already in the iterator is fine. Adding/removing the keyset — despawn, remove, or set on a new entity for one of the iterated components — invalidates the iterator. Always .collect() before structural mutation.
In __DEV__ builds, the iterator tracks the version of every iterated store. If it changes mid-iteration, you get:
[forge] world.query([pos, coin]) iterator detected mid-iteration mutation.Snapshot via .collect() before mutating to avoid undefined behaviour.Production builds (NODE_ENV=production or globalThis.__DEV__ = false) skip the check entirely.
The canonical collect-then-mutate pattern from coin-collector/src/systems/collection.ts:
export const collection_system: System = (w, ctx) => { const players = w.query([pos_c, player_c] as const).collect(); if (players.length === 0) return; const score = ctx.res.get(score_r); if (!score.ok) return;
for (const [, pp] of players) { for (const [cid, cp] of w.query([pos_c, coin_c] as const).collect()) { const dx = pp.x - cp.x; const dy = pp.y - cp.y; if (dx * dx + dy * dy <= radius_sq) { w.despawn(cid); score.value.value += 10; } } }};Query options
Section titled “Query options”The without option excludes entities that have any of the listed components:
const dead = w.query([pos_c] as const, { without: [alive_c] }).collect();Migration from v0.2.0 query_data
Section titled “Migration from v0.2.0 query_data”query_data(data, markers) is removed. query now does the same job — markers in the component list are auto-elided from the tuple:
// v0.2.0for (const [id, p, d] of w.query_data([pos_c, dir_c], [player_c]).collect()) { /* … */ }
// v0.3.0for (const [id, p, d] of w.query([pos_c, dir_c, player_c] as const).collect()) { /* … */ }One array, one as const, identical destructure shape.