@gigzen/populace 1.1.0 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gigzen/populace",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "A simulated population that uses your app through its real API, so you can test what needs more than one person.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -264,9 +264,24 @@ async function run() {
264
264
 
265
265
  console.log(`\n Bringing ${agents} people to life across ${cities.join(", ")}…\n`);
266
266
 
267
+ // Sign-ups are counted here rather than in the engine, so the engine keeps
268
+ // knowing nothing about who is watching.
269
+ let joinedSoFar = 0;
267
270
  const world = World.fromConfig(config, adapter, {
268
- joined: (a) => console.log(` ✓ ${a.persona.name} (${a.persona.city.name}, ${a.persona.platform})`),
269
- joinFailed: (p, e) => console.log(` ${p.name}: ${e.message}`),
271
+ joined: (a) => {
272
+ console.log(` ${a.persona.name} (${a.persona.city.name}, ${a.persona.platform})`);
273
+ progress.joining({
274
+ done: ++joinedSoFar, total: agents, ok: true,
275
+ name: a.persona.name, city: a.persona.city.name,
276
+ });
277
+ },
278
+ joinFailed: (p, e) => {
279
+ console.log(` ✖ ${p.name}: ${e.message}`);
280
+ progress.joining({
281
+ done: ++joinedSoFar, total: agents, ok: false,
282
+ name: p.name, city: p.city?.name,
283
+ });
284
+ },
270
285
  // Said out loud, because waiting silently is indistinguishable from hanging.
271
286
  // Retrying a throttled sign-up can add seconds per person, and a run that
272
287
  // pauses without explanation is a run somebody kills.
@@ -286,10 +301,43 @@ async function run() {
286
301
  },
287
302
  });
288
303
 
289
- process.on("SIGINT", () => {
304
+ const askedToStop = () => {
305
+ if (world.stopping) return;
290
306
  world.stop();
291
307
  console.log(`\n Stopping…\n`);
292
- });
308
+ };
309
+
310
+ process.on("SIGINT", askedToStop);
311
+
312
+ /*
313
+ * A second way to ask, because the first one does not exist on Windows.
314
+ *
315
+ * `child.kill("SIGTERM")` there does not deliver a signal - Windows has no
316
+ * POSIX signals, so Node terminates the process outright whatever name is
317
+ * passed. A parent asking politely therefore killed the run mid-flight and
318
+ * stranded every account it had created, which is the one promise this tool
319
+ * makes loudest. Measured on 2026-08-26: pressing Stop during a 200-person
320
+ * run left all 200 behind.
321
+ *
322
+ * stdin works the same on every platform. A line containing "stop" asks for
323
+ * the same orderly wind-down SIGINT does: finish the tick, tear down, delete
324
+ * the accounts, write the report.
325
+ *
326
+ * unref() so simply having this listener never keeps the process alive, and
327
+ * resume() because a paused stdin emits nothing.
328
+ */
329
+ if (!process.stdin.isTTY) {
330
+ let pending = "";
331
+ process.stdin.on("data", (chunk) => {
332
+ pending += String(chunk);
333
+ const lines = pending.split("\n");
334
+ pending = lines.pop() ?? "";
335
+ if (lines.some((l) => l.trim() === "stop")) askedToStop();
336
+ });
337
+ process.stdin.on("error", () => {}); // a closed pipe is not a reason to fail a run
338
+ process.stdin.resume();
339
+ process.stdin.unref();
340
+ }
293
341
 
294
342
  await world.populate();
295
343
  if (!world.agents.length) {
package/src/progress.mjs CHANGED
@@ -71,7 +71,7 @@ function latencies(metrics) {
71
71
  * @param {(line: string) => void} options.write
72
72
  */
73
73
  export function createProgress({ enabled, everyLatency = 5, write = (l) => process.stdout.write(l) } = {}) {
74
- if (!enabled) return { start() {}, tick() {}, done() {} };
74
+ if (!enabled) return { start() {}, joining() {}, tick() {}, done() {} };
75
75
 
76
76
  const emit = (event) => {
77
77
  try {
@@ -97,6 +97,18 @@ export function createProgress({ enabled, everyLatency = 5, write = (l) => proce
97
97
  });
98
98
  },
99
99
 
100
+ /**
101
+ * One event per person as they sign in, before any tick exists.
102
+ *
103
+ * Signing 250 people in takes minutes, and until this existed a watcher had
104
+ * nothing to show for it: every counter read zero, the progress bar stayed
105
+ * empty and the clock counted down as though the run were already under way.
106
+ * A window that looks identical to a hung one is a window people kill.
107
+ */
108
+ joining({ done, total, name, city, ok }) {
109
+ emit({ type: "joining", done, total, name, city, ok });
110
+ },
111
+
100
112
  tick(tickNo, totalTicks, world, metrics) {
101
113
  const t = world.totals();
102
114
  emit({