@maka/maka-cli 5.137.0 → 5.138.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.137.0",
3
+ "version": "5.138.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -2341,7 +2341,17 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2341
2341
  static HOME_DIRECTION_PRIORITY = [
2342
2342
  Direction.DOWN, Direction.UP, Direction.SOUTH, Direction.NORTH, Direction.EAST, Direction.WEST,
2343
2343
  ];
2344
- constructor(player, sceneSeed) {
2344
+ /**
2345
+ * IN-PROCESS CONDITION REPORTING (2026-09-07): a site-hosted headless
2346
+ * session hands this in so `onConditionChanged` below can skip HubLink
2347
+ * entirely -- see condition-report.ts's `ConditionSink` doc comment for
2348
+ * why this lives on the instance rather than as module state. Absent
2349
+ * for the interactive CLI, which keeps today's HubLink/DDP behavior
2350
+ * unchanged.
2351
+ */
2352
+ _conditionSink;
2353
+ constructor(player, sceneSeed, conditionSink) {
2354
+ this._conditionSink = conditionSink;
2345
2355
  this.player = player;
2346
2356
  this.sceneSeed = sceneSeed;
2347
2357
  this.initialized = false;
@@ -2356,11 +2366,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2356
2366
  return;
2357
2367
  // Checked HERE too, before the import -- a dynamic import after a
2358
2368
  // Jest file's teardown is itself the failure, so the gate cannot
2359
- // live only on the far side of one.
2360
- if (process.env.MAKA_NO_DDP === '1')
2369
+ // live only on the far side of one. Irrelevant when a conditionSink
2370
+ // is wired (no DDP hop for that path to silence).
2371
+ if (!this._conditionSink && process.env.MAKA_NO_DDP === '1')
2361
2372
  return;
2362
2373
  void import('./utilities/condition-report.js')
2363
- .then(m => m.reportConditionSoon(this.player))
2374
+ .then(m => m.reportConditionSoon(this.player, this._conditionSink))
2364
2375
  .catch(() => undefined);
2365
2376
  };
2366
2377
  // A ROOM CHANGE IS A SAVE BEAT (player request 2026-09-02): where you
@@ -18,6 +18,7 @@ import { currentSession, runInSession } from './utilities/session-context.js';
18
18
  import { restorePlayer, serializePlayer } from './utilities/persistence.js';
19
19
  import { renderMapViewport, mapCaption } from './utilities/map-view.js';
20
20
  import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
21
+ import { serializeRoomGrid } from './utilities/room-grid.js';
21
22
  import { entrySpotFor, spotOf } from './utilities/spots.js';
22
23
  import { equipIntoEmptySlots } from './archetypes.js';
23
24
  /**
@@ -191,7 +192,7 @@ export async function createHeadlessSession(opts) {
191
192
  const ctx = { sessionId: opts.sessionId, logger };
192
193
  return runInSession(ctx, async () => {
193
194
  const primary = buildPlayerFromSave(opts.players[0]);
194
- const game = new Game(primary, opts.seed);
195
+ const game = new Game(primary, opts.seed, opts.conditionSink);
195
196
  // The run's tier, stated rather than defaulted -- see
196
197
  // HeadlessOptions.tier and Game.tier.
197
198
  if (typeof opts.tier === 'number')
@@ -325,7 +326,21 @@ export async function createHeadlessSession(opts) {
325
326
  if (roomLines.length > 0) {
326
327
  roomLines.push(mapCaption(roomCaption(here, p)));
327
328
  }
328
- logger.system(roomLines.join('\n'), { actor: p.name }, 'room');
329
+ // STRUCTURED ROOM GEOMETRY, ADDITIVE (2026-09-07): the ASCII
330
+ // above stays the log-adjacent text every consumer already
331
+ // reads; a browser client wanting the site's real SVG DotMap
332
+ // instead reads `data.grid` off this SAME event -- no new
333
+ // event kind, no publication change (GameEvent.data already
334
+ // round-trips end to end). ensureGrid() always synthesizes now
335
+ // ("EVERY ROOM IS MATRIXED", room.ts:357-370, the 2026-08-25
336
+ // ruling that retired the spotless-room compat gap) -- the
337
+ // `undefined` in its return type is a vestige, kept guarded
338
+ // here anyway rather than asserted, since a decoration must
339
+ // never take down the room push (same reasoning as the
340
+ // try/catch this sits inside).
341
+ const grid = here.ensureGrid();
342
+ const data = grid ? { grid: serializeRoomGrid(grid, here.name) } : undefined;
343
+ logger.system(roomLines.join('\n'), { actor: p.name }, 'room', data);
329
344
  }
330
345
  catch (err) {
331
346
  logger.write(`Room overlay render failed for ${p.name} in ${here.name}: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
@@ -18,7 +18,24 @@ import { hostLabel } from './grid-names.js';
18
18
  * storm into a single report of where the monitors LANDED.
19
19
  */
20
20
  const COALESCE_MS = 250;
21
- let pending;
21
+ /**
22
+ * KEYED PER PLAYER, NOT A SINGLE SLOT (fixed 2026-09-07 alongside the
23
+ * ConditionSink work above). A lone real terminal process only ever
24
+ * calls this for its own primary, so a single `pending` variable never
25
+ * showed a problem: every arm captured the SAME player object, and
26
+ * reading its state at fire time was always correct regardless of which
27
+ * call armed the timer. That stops being true the moment more than one
28
+ * `Game` can be reporting condition in the same process -- exactly what
29
+ * ConditionSink exists for. With one shared slot, a second session's
30
+ * mutation arriving while the first session's beat is still gathering
31
+ * hit `if (pending) return` and vanished silently: not delayed, not
32
+ * coalesced into the wrong report -- simply never sent, for that
33
+ * session, for that beat. Keying the map on the Player instance gives
34
+ * each session (each primary Player is a distinct object) its own
35
+ * independent 250ms coalescing window, with no change to the
36
+ * single-session behavior this always had.
37
+ */
38
+ const pendingByPlayer = new WeakMap();
22
39
  /** The method name is the server's -- co-owned; do not rename alone. */
23
40
  export const REPORT_METHOD = 'characterSheet.reportCondition';
24
41
  /**
@@ -215,21 +232,21 @@ function noteRefusal(why) {
215
232
  catch { /* no session */ }
216
233
  }
217
234
  }
218
- export function reportConditionSoon(player) {
235
+ export function reportConditionSoon(player, sink) {
219
236
  // Before the TIMER, not just before the call: a scheduled beat firing
220
237
  // after a Jest file finished lazy-imported into a torn-down
221
238
  // environment and failed CI with every test green (2026-09-01). Under
222
239
  // MAKA_NO_DDP nothing is armed at all -- the same switch the whole
223
- // live layer honors.
224
- if (process.env.MAKA_NO_DDP === '1')
240
+ // live layer honors. Irrelevant to a caller-supplied sink: there is no
241
+ // DDP hop to silence when reporting stays in-process.
242
+ if (!sink && process.env.MAKA_NO_DDP === '1')
225
243
  return;
226
- if (pending)
227
- return; // a beat is already gathering
228
- pending = setTimeout(() => {
229
- pending = undefined;
244
+ if (pendingByPlayer.has(player))
245
+ return; // a beat is already gathering for THIS player
246
+ const timer = setTimeout(() => {
247
+ pendingByPlayer.delete(player);
230
248
  void (async () => {
231
- const [{ HubLink }, { saveSlug }, { arSightUp }] = await Promise.all([
232
- import('./shared-run.js'),
249
+ const [{ saveSlug }, { arSightUp }] = await Promise.all([
233
250
  import('./persistence.js'),
234
251
  import('./ar.js'),
235
252
  ]);
@@ -244,6 +261,14 @@ export function reportConditionSoon(player) {
244
261
  // beat fires next. All read at FIRE time, like position.
245
262
  const report = buildConditionReport(player, { saveSlug, arSightUp });
246
263
  noteRoster(report);
264
+ if (sink) {
265
+ // IN-PROCESS: no HubLink, no MAKA_NO_DDP gate, no version-skew
266
+ // retry -- the engine module and its caller are always the same
267
+ // deployed version here (see ConditionSink's doc comment).
268
+ await sink(report);
269
+ return;
270
+ }
271
+ const { HubLink } = await import('./shared-run.js');
247
272
  try {
248
273
  await HubLink.call(REPORT_METHOD, report);
249
274
  }
@@ -270,10 +295,11 @@ export function reportConditionSoon(player) {
270
295
  noteRefusal(err instanceof Error ? err.message : String(err));
271
296
  });
272
297
  }, COALESCE_MS);
273
- pending.unref?.();
298
+ pendingByPlayer.set(player, timer);
299
+ timer.unref?.();
274
300
  }
275
- /** Test seam: is a beat currently gathering? */
276
- export function conditionReportPending() {
277
- return pending !== undefined;
301
+ /** Test seam: is a beat currently gathering for this player? */
302
+ export function conditionReportPending(player) {
303
+ return pendingByPlayer.has(player);
278
304
  }
279
305
  //# sourceMappingURL=condition-report.js.map
@@ -800,6 +800,37 @@ export function synthesizeGrid(input) {
800
800
  // the module doc); Room.ensureGrid queries this cell by that same
801
801
  // string. A rename on either side must update both.
802
802
  export const OPEN_FLOOR_NAME = 'open floor';
803
+ /** Flattens a live IRoomGrid's Maps into ISerializedRoomGrid. Spots ship
804
+ * UNFILTERED (the caller resolves actor positions against the full
805
+ * list, open floor included -- see room-view.ts's seatingIn); only the
806
+ * DRAWN footprints skip the open-floor anchor, matching the site's own
807
+ * filter. */
808
+ export function serializeRoomGrid(grid, roomName) {
809
+ const spots = [...grid.spotCells.entries()].map(([name, c]) => ({
810
+ name, x: c.x, y: c.y, z: c.z,
811
+ }));
812
+ const doors = [...grid.exitCells.entries()].map(([dir, c]) => ({
813
+ dir: String(dir), x: c.x, y: c.y, z: c.z,
814
+ }));
815
+ const footprints = [...grid.spotFootprints.entries()]
816
+ .filter(([name]) => name !== OPEN_FLOOR_NAME)
817
+ .map(([name, cells]) => ({
818
+ name,
819
+ cells: cells.map(c => ({ x: c.x, y: c.y, z: c.z })),
820
+ }));
821
+ const vertical = [...grid.vertical.entries()].map(([cellKey, link]) => {
822
+ const [x, y, z] = cellKey.split(',').map(Number);
823
+ return { kind: link.kind, x, y, z };
824
+ });
825
+ return {
826
+ name: roomName,
827
+ dims: { x: grid.dims.x, y: grid.dims.y, z: grid.dims.z },
828
+ spots,
829
+ doors,
830
+ footprints,
831
+ vertical,
832
+ };
833
+ }
803
834
  function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); }
804
835
  function jitter(rng) { return Math.floor(rng() * 3) - 1; }
805
836
  function chebyshev(a, b) { return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y)); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.137.0",
3
+ "version": "5.138.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",