@downdraft/engine 0.1.2-alpha.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/app/src/renderer/bootstrap.ts +1 -1
  2. package/app/src/renderer/feature-log.ts +1 -1
  3. package/core/src/input/local-player-manager.ts +2 -2
  4. package/core/src/util/logger.ts +2 -2
  5. package/core/src/worker/sim-worker-base.ts +1 -1
  6. package/dist/.dd-manifest.json +845 -845
  7. package/dist/app/src/renderer/bootstrap.js +1 -1
  8. package/dist/app/src/renderer/feature-log.js +1 -1
  9. package/dist/core/src/input/local-player-manager.js +2 -2
  10. package/dist/core/src/util/logger.js +2 -2
  11. package/dist/core/src/worker/sim-worker-base.js +1 -1
  12. package/dist/libraries/gamepad/src/feed-input.js +1 -1
  13. package/dist/libraries/gamepad/src/hub.js +2 -2
  14. package/dist/libraries/gamepad/src/source-sab.js +2 -2
  15. package/dist/libraries/gamepad/src/sysfs-enrich.js +2 -2
  16. package/dist/libraries/gpu-kernels/src/kernel.js +1 -1
  17. package/dist/libraries/gpu-kernels/src/transpile.js +1 -1
  18. package/dist/libraries/html-ui-kit/src/nav/nav-controller.js +1 -1
  19. package/dist/libraries/html-ui-kit/src/nav/nav-input.js +2 -2
  20. package/dist/libraries/networking/src/discovery.js +8 -8
  21. package/dist/libraries/persistence/src/file-save-store.js +5 -5
  22. package/dist/libraries/persistence/src/opfs-save-store.js +2 -2
  23. package/dist/modules/controller-ui/src/index.js +1 -1
  24. package/dist/modules/devtools/src/blitz/host.js +3 -3
  25. package/dist/modules/devtools/src/blitz/panels/gpu.js +1 -1
  26. package/dist/modules/devtools/src/blitz/panels/performance.js +2 -2
  27. package/dist/modules/html-ui/src/jsx-runtime.js +1 -1
  28. package/libraries/gamepad/src/feed-input.ts +1 -1
  29. package/libraries/gamepad/src/hub.ts +2 -2
  30. package/libraries/gamepad/src/source-sab.ts +2 -2
  31. package/libraries/gamepad/src/sysfs-enrich.ts +2 -2
  32. package/libraries/gpu-kernels/src/kernel.ts +2 -2
  33. package/libraries/gpu-kernels/src/transpile.ts +1 -1
  34. package/libraries/html-ui-kit/src/nav/nav-controller.ts +1 -1
  35. package/libraries/html-ui-kit/src/nav/nav-input.ts +2 -2
  36. package/libraries/networking/src/discovery.ts +8 -8
  37. package/libraries/persistence/src/file-save-store.ts +5 -5
  38. package/libraries/persistence/src/opfs-save-store.ts +2 -2
  39. package/libraries/physics-rapier/src/realm-worker-script.ts +1 -1
  40. package/modules/controller-ui/src/index.ts +1 -1
  41. package/modules/devtools/src/blitz/host.ts +3 -3
  42. package/modules/devtools/src/blitz/panels/gpu.ts +1 -1
  43. package/modules/devtools/src/blitz/panels/performance.ts +2 -2
  44. package/modules/html-ui/src/jsx-runtime.ts +1 -1
  45. package/package.json +2 -2
@@ -429,7 +429,7 @@ async function wireDevtoolsFrontend(mirror, host, renderer, workerHosts, profili
429
429
  }
430
430
  });
431
431
  const evalNames = ["main"];
432
- for (const wh of workerHosts ?? []) {
432
+ for (const wh of (workerHosts ?? []).values()) {
433
433
  const evalFn = wh?.proxy?.__devtoolsEval;
434
434
  if (typeof evalFn === "function" && wh.prefix) {
435
435
  host.registerThreadEval(wh.prefix, (expr) => evalFn.call(wh.proxy, expr));
@@ -134,7 +134,7 @@ export async function getCombinedFeatureLog() {
134
134
  // Single process — merge render fields (wgpu/disp/sab/wk/plug) into the
135
135
  // host line. Host fields win on overlap (v, mode, cpuCores).
136
136
  const host = hostLog || render
137
- ? { ...(render ?? {}), ...(hostLog ?? {}), scope: "host" }
137
+ ? { ...render, ...hostLog, scope: "host" }
138
138
  : null;
139
139
  return {
140
140
  host,
@@ -86,11 +86,11 @@ export class LocalPlayerManager {
86
86
  this.disconnectCallbacks.push(cb);
87
87
  }
88
88
  emitConnect(device) {
89
- for (const cb of this.connectCallbacks)
89
+ for (const cb of this.connectCallbacks.values())
90
90
  cb(device);
91
91
  }
92
92
  emitDisconnect(device) {
93
- for (const cb of this.disconnectCallbacks)
93
+ for (const cb of this.disconnectCallbacks.values())
94
94
  cb(device);
95
95
  }
96
96
  startListening() {
@@ -245,13 +245,13 @@ function highlightJson(json) {
245
245
  }
246
246
  i = end + 1;
247
247
  }
248
- else if (/[\{\}\[\]]/.test(ch)) {
248
+ else if (/[{\}[\]]/.test(ch)) {
249
249
  out += jsonColors().brace + ch + reset;
250
250
  i++;
251
251
  }
252
252
  else if (/\d/.test(ch) || (ch === '-' && /\d/.test(json[i + 1]))) {
253
253
  let end = i + 1;
254
- while (end < json.length && /[\d.eE+\-]/.test(json[end]))
254
+ while (end < json.length && /[\d.eE+-]/.test(json[end]))
255
255
  end++;
256
256
  out += jsonColors().number + json.slice(i, end) + reset;
257
257
  i = end;
@@ -550,7 +550,7 @@ export function createSimWorker(opts) {
550
550
  }
551
551
  : {}),
552
552
  };
553
- const api = { ...standardApi, ...(opts.extraApi ?? {}) };
553
+ const api = { ...standardApi, ...opts.extraApi };
554
554
  // Optionally wrap the API before exposing (e.g. with exposeDevToolsApi
555
555
  // to add devtools + profiling RPC methods).
556
556
  expose(opts.wrapExpose ? opts.wrapExpose(api) : api);
@@ -29,7 +29,7 @@ export function writePadToInput(writer, reader, playerIdx, deviceSlot, bindings,
29
29
  writer.setGamepadButton(playerIdx, b, (d.buttonsLo & (1 << b)) !== 0);
30
30
  }
31
31
  if (bindings) {
32
- for (const bind of bindings) {
32
+ for (const bind of bindings.values()) {
33
33
  let active = false;
34
34
  if (bind.button !== undefined) {
35
35
  active = (d.buttonsLo & (1 << bind.button)) !== 0;
@@ -131,12 +131,12 @@ export class GamepadHub {
131
131
  ffSupported: (s.flags & PAD_FLAG.FF_SUPPORTED) !== 0,
132
132
  });
133
133
  this.devices.set(slot, dev);
134
- for (const cb of this.listeners.connect)
134
+ for (const cb of this.listeners.connect.values())
135
135
  cb(dev);
136
136
  }
137
137
  else if (!connected && existing) {
138
138
  this.devices.delete(slot);
139
- for (const cb of this.listeners.disconnect)
139
+ for (const cb of this.listeners.disconnect.values())
140
140
  cb(existing);
141
141
  }
142
142
  }
@@ -44,11 +44,11 @@ export class SabGamepadSource {
44
44
  if (!(changed & (1 << i)))
45
45
  continue;
46
46
  if (mask & (1 << i)) {
47
- for (const cb of this.connectCbs)
47
+ for (const cb of this.connectCbs.values())
48
48
  cb(i);
49
49
  }
50
50
  else {
51
- for (const cb of this.disconnectCbs)
51
+ for (const cb of this.disconnectCbs.values())
52
52
  cb(i);
53
53
  }
54
54
  }
@@ -54,7 +54,7 @@ async function findBatteryLevel(macHint) {
54
54
  const psDir = "/sys/class/power_supply";
55
55
  if (!existsSync(psDir))
56
56
  return undefined;
57
- for (const entry of await fs.readdir(psDir)) {
57
+ for (const entry of (await fs.readdir(psDir)).values()) {
58
58
  const macPath = join(psDir, entry, "mac");
59
59
  if (existsSync(macPath)) {
60
60
  const mac = (await fs.readFile(macPath, "utf-8")).trim().toLowerCase();
@@ -84,7 +84,7 @@ export class SysfsGamepadEnricher {
84
84
  if (process.platform !== "linux")
85
85
  return [];
86
86
  const out = [];
87
- for (const s of slots) {
87
+ for (const s of slots.values()) {
88
88
  if (s.eventNode < 0)
89
89
  continue;
90
90
  const r = await this.enrichSlot(s);
@@ -100,6 +100,7 @@ export class GpuKernel {
100
100
  const [, oy, oz] = this.outputSize;
101
101
  this.workgroupSize = opts.workgroupSize ?? (oy === 1 && oz === 1 ? [64, 1, 1] : [8, 8, 1]);
102
102
  const [wx, wy, wz] = this.workgroupSize;
103
+ this.label = opts.label ?? "gpu-kernel";
103
104
  const lim = this.device?.limits;
104
105
  if (lim) {
105
106
  const perDim = wx > (lim.maxComputeWorkgroupSizeX ?? wx)
@@ -110,7 +111,6 @@ export class GpuKernel {
110
111
  throw new Error(`kernel "${this.label}": workgroupSize ${wx}×${wy}×${wz} exceeds device limits`);
111
112
  }
112
113
  }
113
- this.label = opts.label ?? "gpu-kernel";
114
114
  this.memoryBudget = opts.memoryBudget;
115
115
  this.extraUsage = opts.outputUsage ?? 0;
116
116
  this.externalResult = opts.outputBuffer ?? null;
@@ -659,7 +659,7 @@ class Emitter {
659
659
  if (e.ty === "vec") {
660
660
  throw new KernelSyntaxError(`arrays can't live in locals — return them directly`, d.init);
661
661
  }
662
- const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool" && e.ty !== "vec";
662
+ const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool";
663
663
  this.declare(name, widen ? "f32" : e.ty);
664
664
  if (node.kind === "const")
665
665
  this.consts.add(name);
@@ -181,7 +181,7 @@ export class NavController {
181
181
  const sign = dir === "down" || dir === "right" ? 1 : -1;
182
182
  let best = null;
183
183
  let bestScore = Infinity;
184
- for (const n of cands) {
184
+ for (const n of cands.values()) {
185
185
  if (n === cur || !n.rect)
186
186
  continue;
187
187
  const r = n.rect;
@@ -29,7 +29,7 @@ export class PadNavDriver {
29
29
  repeatDelayMs: opts.repeatDelayMs ?? 400,
30
30
  repeatIntervalMs: opts.repeatIntervalMs ?? 130,
31
31
  };
32
- for (const d of ["up", "down", "left", "right"]) {
32
+ for (const d of ["up", "down", "left", "right"].values()) {
33
33
  this.dirs.set(d, { held: false, since: 0, lastFire: 0 });
34
34
  }
35
35
  }
@@ -67,7 +67,7 @@ export class PadNavDriver {
67
67
  this.prevDirAxis = stickDir;
68
68
  if (stickDir)
69
69
  pressed.add(stickDir);
70
- for (const [dir, st] of this.dirs) {
70
+ for (const [dir, st] of this.dirs.entries()) {
71
71
  const isDown = pressed.has(dir);
72
72
  if (isDown && !st.held) {
73
73
  st.held = true;
@@ -165,20 +165,20 @@ export class LanDiscovery {
165
165
  const peer = { ...pkt, host: rinfo.address, lastSeenMs: this.opts.now() };
166
166
  this.peers.set(pkt.id, peer);
167
167
  if (!existing)
168
- for (const f of this.listeners.join)
168
+ for (const f of this.listeners.join.values())
169
169
  f(peer);
170
170
  else if (existing.name !== peer.name || existing.port !== peer.port
171
171
  || JSON.stringify(existing.meta) !== JSON.stringify(peer.meta)) {
172
- for (const f of this.listeners.update)
172
+ for (const f of this.listeners.update.values())
173
173
  f(peer);
174
174
  }
175
175
  }
176
176
  expire() {
177
177
  const cutoff = this.opts.now() - this.opts.ttlMs;
178
- for (const [id, p] of this.peers) {
178
+ for (const [id, p] of this.peers.entries()) {
179
179
  if (p.lastSeenMs < cutoff) {
180
180
  this.peers.delete(id);
181
- for (const f of this.listeners.leave)
181
+ for (const f of this.listeners.leave.values())
182
182
  f(p);
183
183
  }
184
184
  }
@@ -227,7 +227,7 @@ export class MdnsBrowser {
227
227
  this.sendQuery(type);
228
228
  }
229
229
  requery() {
230
- for (const t of this.watched)
230
+ for (const t of this.watched.values())
231
231
  this.sendQuery(t);
232
232
  this.expire();
233
233
  }
@@ -257,7 +257,7 @@ export class MdnsBrowser {
257
257
  }
258
258
  expire() {
259
259
  const now = this.opts.now();
260
- for (const [k, s] of this.table) {
260
+ for (const [k, s] of this.table.entries()) {
261
261
  if (s.lastSeenMs + s.ttlMs < now)
262
262
  this.table.delete(k);
263
263
  }
@@ -276,7 +276,7 @@ export class MdnsBrowser {
276
276
  .filter((r) => "data" in r);
277
277
  const now = this.opts.now();
278
278
  // PTR gives instance names; SRV/TXT/A for the same instance fill detail.
279
- for (const r of recs) {
279
+ for (const r of recs.values()) {
280
280
  if (r.type !== "PTR" || !this.watched.has(r.name))
281
281
  continue;
282
282
  const instance = r.data;
@@ -291,7 +291,7 @@ export class MdnsBrowser {
291
291
  .map((x) => x.ttl ?? 120)));
292
292
  const txtMap = {};
293
293
  if (txt) {
294
- for (const item of txt.data) {
294
+ for (const item of txt.data.values()) {
295
295
  const s = item.toString("utf8");
296
296
  const eq = s.indexOf("=");
297
297
  txtMap[eq < 0 ? s : s.slice(0, eq)] = eq < 0 ? "" : s.slice(eq + 1);
@@ -152,7 +152,7 @@ export class FileSaveStore {
152
152
  await fs.mkdir(tmpBlobsDir, { recursive: true });
153
153
  for (let _i = 0, _it = Object.entries(opts.blobs), _n = _it.length; _i < _n; _i++) {
154
154
  const [key, buf] = _it[_i];
155
- const safeKey = key.replace(/[^a-zA-Z0-9_\-]/g, "_");
155
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
156
156
  await fs.writeFile(join(tmpBlobsDir, safeKey), new Uint8Array(buf));
157
157
  blobBytes += buf.byteLength;
158
158
  }
@@ -480,19 +480,19 @@ export class FileSaveStore {
480
480
  }
481
481
  slotPath(slot) {
482
482
  // Sanitize slot name to prevent path traversal
483
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
483
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
484
484
  return join(this.saveDir, safe + SAVE_EXT);
485
485
  }
486
486
  blobsDirPath(slot) {
487
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
487
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
488
488
  return join(this.saveDir, safe + BLOBS_DIR_SUFFIX);
489
489
  }
490
490
  thumbPath(slot) {
491
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
491
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
492
492
  return join(this.saveDir, safe + THUMB_EXT);
493
493
  }
494
494
  propsPath(slot) {
495
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
495
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
496
496
  return join(this.saveDir, safe + PROPS_EXT);
497
497
  }
498
498
  hashEqual(a, b) {
@@ -147,7 +147,7 @@ export class OpfsSaveStore {
147
147
  }
148
148
  // ── Path helpers ────────────────────────────────────────────────────────
149
149
  sanitizeSlot(slot) {
150
- return slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
150
+ return slot.replace(/[^a-zA-Z0-9_-]/g, "_");
151
151
  }
152
152
  genDirName(gen) {
153
153
  return String(gen).padStart(4, "0");
@@ -239,7 +239,7 @@ export class OpfsSaveStore {
239
239
  if (opts?.blobs && Object.keys(opts.blobs).length > 0) {
240
240
  const blobsDir = await this.getBlobsDir(genDir, true);
241
241
  for (const [key, buf] of Object.entries(opts.blobs)) {
242
- const safeKey = key.replace(/[^a-zA-Z0-9_\-]/g, "_");
242
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
243
243
  await this.writeBinaryFile(blobsDir, safeKey, new Uint8Array(buf));
244
244
  blobCount++;
245
245
  }
@@ -125,7 +125,7 @@ export function createControllerUi() {
125
125
  if (!p || !dev)
126
126
  return;
127
127
  const ops = [];
128
- for (const [bit] of BTN_LABELS) {
128
+ for (const [bit] of BTN_LABELS.values()) {
129
129
  ops.push({ op: "attr", sel: `#ctl-btn-${bit}`, name: "data-on",
130
130
  value: dev.pressed(bit) ? "true" : "false" });
131
131
  }
@@ -76,7 +76,7 @@ export class BlitzDevtoolsHost {
76
76
  new ElementsPanel(ctx),
77
77
  new PerformancePanel(ctx),
78
78
  new GpuPanel(ctx),
79
- ])
79
+ ].values())
80
80
  this.panels.set(p.id, p);
81
81
  }
82
82
  get visible() { return this.panel !== null; }
@@ -252,7 +252,7 @@ export class BlitzDevtoolsHost {
252
252
  const slots = this.backend.providerSlots();
253
253
  let changed = false;
254
254
  const seen = new Set();
255
- for (const { slot, name } of slots) {
255
+ for (const { slot, name } of slots.values()) {
256
256
  seen.add(slot);
257
257
  if (!this.snapshotPanels.has(slot)) {
258
258
  const p = new SnapshotPanel(this.panelCtx, slot, name);
@@ -261,7 +261,7 @@ export class BlitzDevtoolsHost {
261
261
  changed = true;
262
262
  }
263
263
  }
264
- for (const [slot, p] of this.snapshotPanels) {
264
+ for (const [slot, p] of this.snapshotPanels.entries()) {
265
265
  if (!seen.has(slot)) {
266
266
  this.snapshotPanels.delete(slot);
267
267
  this.panels.delete(p.id);
@@ -22,7 +22,7 @@ export class GpuPanel {
22
22
  const times = (this.info.frameTimes ?? []).map((t) => t[0]);
23
23
  const chart = `<div class="snap-section"><h3>Frame times (ms)</h3>${chartHtml(times, { max: Math.max(16.7, ...times), label: "" })}</div>`;
24
24
  const sections = [];
25
- for (const e of this.info.entries ?? []) {
25
+ for (const e of (this.info.entries ?? []).values()) {
26
26
  if (e.isHeader || !sections.length) {
27
27
  sections.push({ header: e.isHeader ? e.key : null, rows: [] });
28
28
  if (e.isHeader)
@@ -36,7 +36,7 @@ export class PerformancePanel {
36
36
  let tbl = `<table class="dt"><tr>
37
37
  <th>slot</th><th>name</th><th>cpu%</th><th>heap</th>
38
38
  <th>gc max µs</th><th>task p95 µs</th></tr>`;
39
- for (const s of this.slots) {
39
+ for (const s of this.slots.values()) {
40
40
  const h = s.history?.[0] ?? {};
41
41
  tbl += `<tr><td>${s.slotIndex}</td><td>${esc(s.name)}</td>
42
42
  <td>${(h.cpuPercent ?? 0).toFixed(1)}</td>
@@ -62,7 +62,7 @@ export class PerformancePanel {
62
62
  onBackendEvent(event, data) {
63
63
  if (event === "metrics") {
64
64
  this.slots = data ?? [];
65
- for (const s of this.slots) {
65
+ for (const s of this.slots.values()) {
66
66
  const h = s.history?.[0];
67
67
  if (!h)
68
68
  continue;
@@ -98,7 +98,7 @@ function renderNode(v) {
98
98
  return out.join("");
99
99
  }
100
100
  if (typeof t === "function") {
101
- const r = t({ ...(p ?? {}) });
101
+ const r = t({ ...p });
102
102
  const out = [];
103
103
  renderInner(r, out);
104
104
  return out.join("");
@@ -57,7 +57,7 @@ export function writePadToInput(
57
57
  writer.setGamepadButton(playerIdx, b, (d.buttonsLo & (1 << b)) !== 0);
58
58
  }
59
59
  if (bindings) {
60
- for (const bind of bindings) {
60
+ for (const bind of bindings.values()) {
61
61
  let active = false;
62
62
  if (bind.button !== undefined) {
63
63
  active = (d.buttonsLo & (1 << bind.button)) !== 0;
@@ -171,10 +171,10 @@ export class GamepadHub {
171
171
  ffSupported: (s.flags & PAD_FLAG.FF_SUPPORTED) !== 0,
172
172
  });
173
173
  this.devices.set(slot, dev);
174
- for (const cb of this.listeners.connect) cb(dev);
174
+ for (const cb of this.listeners.connect.values()) cb(dev);
175
175
  } else if (!connected && existing) {
176
176
  this.devices.delete(slot);
177
- for (const cb of this.listeners.disconnect) cb(existing);
177
+ for (const cb of this.listeners.disconnect.values()) cb(existing);
178
178
  }
179
179
  }
180
180
  this.lastSeq = this.reader.getSequence();
@@ -53,9 +53,9 @@ export class SabGamepadSource implements GamepadSource {
53
53
  for (let i = 0; i < GAMEPAD_MAX_SLOTS; i++) {
54
54
  if (!(changed & (1 << i))) continue;
55
55
  if (mask & (1 << i)) {
56
- for (const cb of this.connectCbs) cb(i);
56
+ for (const cb of this.connectCbs.values()) cb(i);
57
57
  } else {
58
- for (const cb of this.disconnectCbs) cb(i);
58
+ for (const cb of this.disconnectCbs.values()) cb(i);
59
59
  }
60
60
  }
61
61
  this.knownMask = mask;
@@ -58,7 +58,7 @@ async function findBatteryLevel(macHint: string | null): Promise<number | undefi
58
58
  try {
59
59
  const psDir = "/sys/class/power_supply";
60
60
  if (!existsSync(psDir)) return undefined;
61
- for (const entry of await fs.readdir(psDir)) {
61
+ for (const entry of (await fs.readdir(psDir)).values()) {
62
62
  const macPath = join(psDir, entry, "mac");
63
63
  if (existsSync(macPath)) {
64
64
  const mac = (await fs.readFile(macPath, "utf-8")).trim().toLowerCase();
@@ -98,7 +98,7 @@ export class SysfsGamepadEnricher {
98
98
  async enrich(slots: GamepadDeviceSlot[]): Promise<EnrichResult[]> {
99
99
  if (process.platform !== "linux") return [];
100
100
  const out: EnrichResult[] = [];
101
- for (const s of slots) {
101
+ for (const s of slots.values()) {
102
102
  if (s.eventNode < 0) continue;
103
103
  const r = await this.enrichSlot(s);
104
104
  out.push(r);
@@ -177,6 +177,7 @@ export class GpuKernel {
177
177
  const [, oy, oz] = this.outputSize;
178
178
  this.workgroupSize = opts.workgroupSize ?? (oy === 1 && oz === 1 ? [64, 1, 1] : [8, 8, 1]);
179
179
  const [wx, wy, wz] = this.workgroupSize;
180
+ this.label = opts.label ?? "gpu-kernel";
180
181
  const lim = this.device?.limits as {
181
182
  maxComputeWorkgroupSizeX?: number;
182
183
  maxComputeWorkgroupSizeY?: number;
@@ -192,7 +193,6 @@ export class GpuKernel {
192
193
  throw new Error(`kernel "${this.label}": workgroupSize ${wx}×${wy}×${wz} exceeds device limits`);
193
194
  }
194
195
  }
195
- this.label = opts.label ?? "gpu-kernel";
196
196
  this.memoryBudget = opts.memoryBudget;
197
197
  this.extraUsage = opts.outputUsage ?? 0;
198
198
  this.externalResult = opts.outputBuffer ?? null;
@@ -586,7 +586,7 @@ export class GpuKernel {
586
586
  // Pre-validate against device limits — Adreno-class GPUs allow as few as
587
587
  // 16 storage buffers/stage and 256MB/binding, and wgpu otherwise reports a
588
588
  // generic "command buffer failed validation" at submit time.
589
- const limits = this.device.limits as Record<string, number | undefined> | undefined;
589
+ const limits = this.device.limits as unknown as Record<string, number | undefined> | undefined;
590
590
  const maxStorage = limits?.maxStorageBuffersPerShaderStage ?? Infinity;
591
591
  const maxBinding = limits?.maxStorageBufferBindingSize ?? Infinity;
592
592
  const storageCount = unit.transpiled.bindings.length
@@ -720,7 +720,7 @@ class Emitter {
720
720
  if (e.ty === "vec") {
721
721
  throw new KernelSyntaxError(`arrays can't live in locals — return them directly`, d.init);
722
722
  }
723
- const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool" && e.ty !== "vec";
723
+ const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool";
724
724
  this.declare(name, widen ? "f32" : e.ty);
725
725
  if (node.kind === "const") this.consts.add(name);
726
726
  const kw = node.kind === "const" ? "let" : "var";
@@ -209,7 +209,7 @@ export class NavController {
209
209
 
210
210
  let best: NavNodeInfo | null = null;
211
211
  let bestScore = Infinity;
212
- for (const n of cands) {
212
+ for (const n of cands.values()) {
213
213
  if (n === cur || !n.rect) continue;
214
214
  const r = n.rect;
215
215
  const nx = r.x + r.w / 2, ny = r.y + r.h / 2;
@@ -50,7 +50,7 @@ export class PadNavDriver {
50
50
  repeatDelayMs: opts.repeatDelayMs ?? 400,
51
51
  repeatIntervalMs: opts.repeatIntervalMs ?? 130,
52
52
  };
53
- for (const d of ["up", "down", "left", "right"] as const) {
53
+ for (const d of (["up", "down", "left", "right"] as const).values()) {
54
54
  this.dirs.set(d, { held: false, since: 0, lastFire: 0 });
55
55
  }
56
56
  }
@@ -82,7 +82,7 @@ export class PadNavDriver {
82
82
  this.prevDirAxis = stickDir;
83
83
  if (stickDir) pressed.add(stickDir);
84
84
 
85
- for (const [dir, st] of this.dirs) {
85
+ for (const [dir, st] of this.dirs.entries()) {
86
86
  const isDown = pressed.has(dir);
87
87
  if (isDown && !st.held) {
88
88
  st.held = true; st.since = now; st.lastFire = now;
@@ -206,19 +206,19 @@ export class LanDiscovery {
206
206
  const existing = this.peers.get(pkt.id);
207
207
  const peer: LanPeer = { ...pkt, host: rinfo.address, lastSeenMs: this.opts.now() };
208
208
  this.peers.set(pkt.id, peer);
209
- if (!existing) for (const f of this.listeners.join) f(peer);
209
+ if (!existing) for (const f of this.listeners.join.values()) f(peer);
210
210
  else if (existing.name !== peer.name || existing.port !== peer.port
211
211
  || JSON.stringify(existing.meta) !== JSON.stringify(peer.meta)) {
212
- for (const f of this.listeners.update) f(peer);
212
+ for (const f of this.listeners.update.values()) f(peer);
213
213
  }
214
214
  }
215
215
 
216
216
  private expire() {
217
217
  const cutoff = this.opts.now() - this.opts.ttlMs;
218
- for (const [id, p] of this.peers) {
218
+ for (const [id, p] of this.peers.entries()) {
219
219
  if (p.lastSeenMs < cutoff) {
220
220
  this.peers.delete(id);
221
- for (const f of this.listeners.leave) f(p);
221
+ for (const f of this.listeners.leave.values()) f(p);
222
222
  }
223
223
  }
224
224
  }
@@ -292,7 +292,7 @@ export class MdnsBrowser {
292
292
  }
293
293
 
294
294
  private requery() {
295
- for (const t of this.watched) this.sendQuery(t);
295
+ for (const t of this.watched.values()) this.sendQuery(t);
296
296
  this.expire();
297
297
  }
298
298
 
@@ -320,7 +320,7 @@ export class MdnsBrowser {
320
320
 
321
321
  private expire() {
322
322
  const now = this.opts.now();
323
- for (const [k, s] of this.table) {
323
+ for (const [k, s] of this.table.entries()) {
324
324
  if (s.lastSeenMs + s.ttlMs < now) this.table.delete(k);
325
325
  }
326
326
  }
@@ -334,7 +334,7 @@ export class MdnsBrowser {
334
334
  .filter((r): r is Extract<typeof r, { data: unknown }> => "data" in r);
335
335
  const now = this.opts.now();
336
336
  // PTR gives instance names; SRV/TXT/A for the same instance fill detail.
337
- for (const r of recs) {
337
+ for (const r of recs.values()) {
338
338
  if (r.type !== "PTR" || !this.watched.has(r.name)) continue;
339
339
  const instance = r.data as string;
340
340
  const key = `${instance}@${r.name}`;
@@ -350,7 +350,7 @@ export class MdnsBrowser {
350
350
  );
351
351
  const txtMap: Record<string, string> = {};
352
352
  if (txt) {
353
- for (const item of (txt.data as Buffer[])) {
353
+ for (const item of (txt.data as Buffer[]).values()) {
354
354
  const s = item.toString("utf8");
355
355
  const eq = s.indexOf("=");
356
356
  txtMap[eq < 0 ? s : s.slice(0, eq)] = eq < 0 ? "" : s.slice(eq + 1);
@@ -205,7 +205,7 @@ export class FileSaveStore implements ISaveStore {
205
205
  if (opts?.blobs && Object.keys(opts.blobs).length > 0) {
206
206
  await fs.mkdir(tmpBlobsDir, { recursive: true });
207
207
  for (let _i = 0, _it = Object.entries(opts.blobs) as Array<[string, ArrayBuffer]>, _n = _it.length; _i < _n; _i++) { const [key, buf] = _it[_i];
208
- const safeKey = key.replace(/[^a-zA-Z0-9_\-]/g, "_");
208
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
209
209
  await fs.writeFile(join(tmpBlobsDir, safeKey), new Uint8Array(buf));
210
210
  blobBytes += buf.byteLength;
211
211
  }
@@ -524,22 +524,22 @@ export class FileSaveStore implements ISaveStore {
524
524
 
525
525
  private slotPath(slot: string): string {
526
526
  // Sanitize slot name to prevent path traversal
527
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
527
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
528
528
  return join(this.saveDir, safe + SAVE_EXT);
529
529
  }
530
530
 
531
531
  private blobsDirPath(slot: string): string {
532
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
532
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
533
533
  return join(this.saveDir, safe + BLOBS_DIR_SUFFIX);
534
534
  }
535
535
 
536
536
  private thumbPath(slot: string): string {
537
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
537
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
538
538
  return join(this.saveDir, safe + THUMB_EXT);
539
539
  }
540
540
 
541
541
  private propsPath(slot: string): string {
542
- const safe = slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
542
+ const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
543
543
  return join(this.saveDir, safe + PROPS_EXT);
544
544
  }
545
545
 
@@ -262,7 +262,7 @@ export class OpfsSaveStore implements ISaveStore {
262
262
  // ── Path helpers ────────────────────────────────────────────────────────
263
263
 
264
264
  private sanitizeSlot(slot: string): string {
265
- return slot.replace(/[^a-zA-Z0-9_\-]/g, "_");
265
+ return slot.replace(/[^a-zA-Z0-9_-]/g, "_");
266
266
  }
267
267
 
268
268
  private genDirName(gen: number): string {
@@ -371,7 +371,7 @@ export class OpfsSaveStore implements ISaveStore {
371
371
  if (opts?.blobs && Object.keys(opts.blobs).length > 0) {
372
372
  const blobsDir = await this.getBlobsDir(genDir, true);
373
373
  for (const [key, buf] of Object.entries(opts.blobs)) {
374
- const safeKey = key.replace(/[^a-zA-Z0-9_\-]/g, "_");
374
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
375
375
  await this.writeBinaryFile(blobsDir, safeKey, new Uint8Array(buf));
376
376
  blobCount++;
377
377
  }
@@ -233,5 +233,5 @@ self.onmessage = async (e: MessageEvent) => {
233
233
  }
234
234
  };
235
235
 
236
- export { };
236
+
237
237
 
@@ -138,7 +138,7 @@ export function createControllerUi(): RendererModule {
138
138
  const dev = selected();
139
139
  if (!p || !dev) return;
140
140
  const ops = [];
141
- for (const [bit] of BTN_LABELS) {
141
+ for (const [bit] of BTN_LABELS.values()) {
142
142
  ops.push({ op: "attr", sel: `#ctl-btn-${bit}`, name: "data-on",
143
143
  value: dev.pressed(bit) ? "true" : "false" } as const);
144
144
  }