@threadcrumbs/threadnauts 1.1.0 → 1.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.
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <meta name="color-scheme" content="dark" />
7
7
  <title>Threadnauts</title>
8
8
  <link rel="icon" href="/favicon.svg?app=threadnauts" type="image/svg+xml" />
9
- <script type="module" crossorigin src="/assets/index-BSPNlXcc.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/shared-CgqOYNxw.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-DoVfuOrP.css">
9
+ <script type="module" crossorigin src="/assets/index-C46d6g6p.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/shared-DiTtF988.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-PzTcyxAK.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="app"><div class="boot"><div class="inner">
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "files": [
3
3
  {
4
- "path": "assets/pro-Cb7jFyCP.js",
5
- "sha256": "83dcd0c67e85a2e5b2d8130f86afda3291e10f34e6727ac5157f2ee4dfc72a4b",
6
- "bytes": 188138
4
+ "path": "assets/pro-lBnPFibL.js",
5
+ "sha256": "e8e7df01da6e616fb37a60ddc2657b63681cfd34adf8fb37116ecad906845a6e",
6
+ "bytes": 233030
7
7
  }
8
8
  ]
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadcrumbs/threadnauts",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "A 3D colony sim where every coding-agent thread on your machine is a little bot building something.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -49,7 +49,8 @@
49
49
  "icons": "node tools/build-world-icons.mjs",
50
50
  "site": "node tools/build-site.mjs",
51
51
  "test": "node --test \"test/**/*.test.mjs\"",
52
- "prepack": "pnpm run build"
52
+ "prepack": "pnpm run build",
53
+ "auto-publish": "node ../common/tools/release.mjs"
53
54
  },
54
55
  "dependencies": {
55
56
  "@threadcrumbs/common": "^0.1.0"
package/server/api.mjs CHANGED
@@ -90,9 +90,26 @@ const emptyState = () => ({
90
90
  const asObject = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {})
91
91
  const asArray = (v) => (Array.isArray(v) ? v : [])
92
92
 
93
+ const BUSY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
94
+ const BUSY_WAITS_MS = [50, 150, 400]
95
+ const BUSY_MESSAGE = 'Another program has colony.json open. Threadnauts will try again.'
96
+ const isBusy = (err) => BUSY_CODES.has(err?.code)
97
+
98
+ async function whileBusy(operation) {
99
+ for (const wait of BUSY_WAITS_MS) {
100
+ try {
101
+ return await operation()
102
+ } catch (err) {
103
+ if (!isBusy(err)) throw err
104
+ await new Promise((resolve) => setTimeout(resolve, wait))
105
+ }
106
+ }
107
+ return operation()
108
+ }
109
+
93
110
  async function readState() {
94
111
  try {
95
- const raw = migrate(JSON.parse(await fsp.readFile(STATE_FILE, 'utf8')))
112
+ const raw = migrate(JSON.parse(await whileBusy(() => fsp.readFile(STATE_FILE, 'utf8'))))
96
113
  return {
97
114
  version: STATE_VERSION,
98
115
  archived: asArray(raw.archived),
@@ -109,7 +126,8 @@ async function readState() {
109
126
  settings: raw.settings && typeof raw.settings === 'object' ? raw.settings : null,
110
127
  updatedAt: Number(raw.updatedAt) || 0,
111
128
  }
112
- } catch {
129
+ } catch (err) {
130
+ if (isBusy(err)) throw err
113
131
  return emptyState()
114
132
  }
115
133
  }
@@ -154,7 +172,7 @@ async function writeState(next) {
154
172
  // colony.json names the repos you work in and where they are: the owner's business, not
155
173
  // every account on the machine's. A fresh file per write, so the mode always applies.
156
174
  await fsp.writeFile(tmp, JSON.stringify(state, null, 2), { mode: 0o600 })
157
- await fsp.rename(tmp, STATE_FILE)
175
+ await whileBusy(() => fsp.rename(tmp, STATE_FILE))
158
176
  } catch (err) {
159
177
  await fsp.rm(tmp, { force: true }).catch(() => {})
160
178
  throw err
@@ -477,7 +495,7 @@ export async function middleware(req, res, next) {
477
495
  if (url.pathname === '/api/state' && req.method === 'PUT') {
478
496
  const body = await readJsonBody(req)
479
497
  const base = Number(body.baseUpdatedAt) || 0
480
- return serialise(async () => {
498
+ return await serialise(async () => {
481
499
  const current = await readState()
482
500
  if (base && current.updatedAt !== base) return send(res, 409, current)
483
501
  return send(res, 200, await writeState(body))
@@ -535,6 +553,7 @@ export async function middleware(req, res, next) {
535
553
 
536
554
  return send(res, 404, { error: 'Unknown endpoint' })
537
555
  } catch (err) {
556
+ if (isBusy(err)) return send(res, 503, { error: BUSY_MESSAGE })
538
557
  return send(res, 500, { error: String(err && err.message ? err.message : err) })
539
558
  }
540
559
  }