@gigzen/populace 1.2.0 → 1.3.1

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,60 +1,60 @@
1
- {
2
- "name": "@gigzen/populace",
3
- "version": "1.2.0",
4
- "description": "A simulated population that uses your app through its real API, so you can test what needs more than one person.",
5
- "type": "module",
6
- "bin": {
7
- "populace": "./src/cli.mjs"
8
- },
9
- "main": "./src/index.mjs",
10
- "exports": {
11
- ".": "./src/index.mjs",
12
- "./engine": "./src/engine/index.mjs"
13
- },
14
- "files": [
15
- "src",
16
- "adapters",
17
- "examples",
18
- "!**/populace-report.json",
19
- "!**/populace-report.html",
20
- "!examples/buzzbuzz/run-test.ps1",
21
- "populace.config.example.mjs",
22
- "README.md",
23
- "LICENSE",
24
- "action.yml"
25
- ],
26
- "scripts": {
27
- "demo": "node src/cli.mjs demo",
28
- "doctor": "node src/cli.mjs doctor",
29
- "test": "node src/selftest.mjs",
30
- "prepublishOnly": "node src/selftest.mjs"
31
- },
32
- "engines": {
33
- "node": ">=18"
34
- },
35
- "keywords": [
36
- "simulation",
37
- "testing",
38
- "load-testing",
39
- "multi-user",
40
- "synthetic-users",
41
- "qa"
42
- ],
43
- "license": "AGPL-3.0-only",
44
- "dependencies": {},
45
- "devDependencies": {
46
- "@supabase/supabase-js": "^2.45.0"
47
- },
48
- "author": "Sankur Kundu <sankur.kundu.tw@gmail.com>",
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/Shakhtar-Sankur/populace.git"
52
- },
53
- "homepage": "https://github.com/Shakhtar-Sankur/populace#readme",
54
- "bugs": {
55
- "url": "https://github.com/Shakhtar-Sankur/populace/issues"
56
- },
57
- "publishConfig": {
58
- "access": "public"
59
- }
60
- }
1
+ {
2
+ "name": "@gigzen/populace",
3
+ "version": "1.3.1",
4
+ "description": "A simulated population that uses your app through its real API, so you can test what needs more than one person.",
5
+ "type": "module",
6
+ "bin": {
7
+ "populace": "./src/cli.mjs"
8
+ },
9
+ "main": "./src/index.mjs",
10
+ "exports": {
11
+ ".": "./src/index.mjs",
12
+ "./engine": "./src/engine/index.mjs"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "adapters",
17
+ "examples",
18
+ "!**/populace-report.json",
19
+ "!**/populace-report.html",
20
+ "!examples/buzzbuzz/run-test.ps1",
21
+ "populace.config.example.mjs",
22
+ "README.md",
23
+ "LICENSE",
24
+ "action.yml"
25
+ ],
26
+ "scripts": {
27
+ "demo": "node src/cli.mjs demo",
28
+ "doctor": "node src/cli.mjs doctor",
29
+ "test": "node src/selftest.mjs",
30
+ "prepublishOnly": "node src/selftest.mjs"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "keywords": [
36
+ "simulation",
37
+ "testing",
38
+ "load-testing",
39
+ "multi-user",
40
+ "synthetic-users",
41
+ "qa"
42
+ ],
43
+ "license": "AGPL-3.0-only",
44
+ "dependencies": {},
45
+ "devDependencies": {
46
+ "@supabase/supabase-js": "^2.45.0"
47
+ },
48
+ "author": "Sankur Kundu <sankur.kundu.tw@gmail.com>",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/Shakhtar-Sankur/populace.git"
52
+ },
53
+ "homepage": "https://github.com/Shakhtar-Sankur/populace#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/Shakhtar-Sankur/populace/issues"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
package/src/cli.mjs CHANGED
@@ -301,10 +301,53 @@ async function run() {
301
301
  },
302
302
  });
303
303
 
304
- process.on("SIGINT", () => {
304
+ const askedToStop = () => {
305
+ if (world.stopping) return;
305
306
  world.stop();
306
307
  console.log(`\n Stopping…\n`);
307
- });
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
+ /* unref() is NOT on every stdin. Node hands you a different stream
340
+ depending on what stdin is attached to, and only some of them are
341
+ sockets: with stdin redirected from a file or the null device — CI
342
+ runners, `docker run` without -i, most automation — it is an
343
+ fs.ReadStream, which has no unref, and calling it threw
344
+ "process.stdin.unref is not a function" before the run had started.
345
+ This whole branch exists to make non-interactive use work, so it was
346
+ failing in exactly the case it was written for. Nothing is lost when it
347
+ is absent: those streams reach EOF immediately and never hold the loop
348
+ open, which is all unref was buying. */
349
+ process.stdin.unref?.();
350
+ }
308
351
 
309
352
  await world.populate();
310
353
  if (!world.agents.length) {
@@ -78,20 +78,20 @@ export function renderHtmlReport(report) {
78
78
  <style>
79
79
  :root{
80
80
  --paper:#F4F5F7;--card:#fff;--ink:#12181D;--body:#3A454E;--muted:#6B7883;
81
- --rule:#DDE2E7;--accent:#A85A0B;--ok:#2C6E4C;--bad:#A63127;--warn:#9A6A08;
81
+ --rule:#DDE2E7;--accent:#4338CA;--ok:#2C6E4C;--bad:#A63127;--warn:#9A6A08;
82
82
  --bar:#C9D4DC;--barbad:#E0A99F;
83
83
  --mono:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
84
84
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
85
85
  }
86
86
  @media (prefers-color-scheme:dark){:root{
87
87
  --paper:#0E1418;--card:#161E24;--ink:#E8EEF3;--body:#B0BDC7;--muted:#7B8994;
88
- --rule:#26323A;--accent:#E0A45C;--ok:#5FAB80;--bad:#DE7568;--warn:#D2A24E;
88
+ --rule:#26323A;--accent:#A5B4FC;--ok:#5FAB80;--bad:#DE7568;--warn:#D2A24E;
89
89
  --bar:#2E3A43;--barbad:#5C332C;}}
90
90
  :root[data-theme="dark"]{--paper:#0E1418;--card:#161E24;--ink:#E8EEF3;--body:#B0BDC7;
91
- --muted:#7B8994;--rule:#26323A;--accent:#E0A45C;--ok:#5FAB80;--bad:#DE7568;
91
+ --muted:#7B8994;--rule:#26323A;--accent:#A5B4FC;--ok:#5FAB80;--bad:#DE7568;
92
92
  --warn:#D2A24E;--bar:#2E3A43;--barbad:#5C332C;}
93
93
  :root[data-theme="light"]{--paper:#F4F5F7;--card:#fff;--ink:#12181D;--body:#3A454E;
94
- --muted:#6B7883;--rule:#DDE2E7;--accent:#A85A0B;--ok:#2C6E4C;--bad:#A63127;
94
+ --muted:#6B7883;--rule:#DDE2E7;--accent:#4338CA;--ok:#2C6E4C;--bad:#A63127;
95
95
  --warn:#9A6A08;--bar:#C9D4DC;--barbad:#E0A99F;}
96
96
  *{box-sizing:border-box}
97
97
  body{margin:0;background:var(--paper);color:var(--body);font-family:var(--sans);