mikser-io 9.2.1 → 9.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": "mikser-io",
3
- "version": "9.2.1",
3
+ "version": "9.3.0",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -394,6 +394,37 @@ export function api(options = {}) {
394
394
  // api.endpoints.admin { token: '...', operations: ['list','update','delete','render'] }
395
395
  for (const [name, ep] of Object.entries(endpoints)) {
396
396
  const router = express.Router()
397
+
398
+ // Nothing is served until the first build cycle has finished.
399
+ //
400
+ // The server binds at the end of the loaded phase — before
401
+ // process() has emitted an entity — so without this the endpoint
402
+ // spends the whole first build answering against an empty catalog.
403
+ // Renders fail outright (the layouts registry fills during
404
+ // process()), and reads are worse than that: a list returns
405
+ // whichever subset exists at that instant, which is a wrong answer
406
+ // wearing a 200. A consumer seeding its routes from that gets zero
407
+ // routes and renders blank pages.
408
+ //
409
+ // 503, not 4xx, and Retry-After: this is the one honest status
410
+ // here. It says the request was fine and the server was not, which
411
+ // is exactly the case, and it is the status every HTTP client
412
+ // already knows to retry. A 422 or a 500 invites the caller to
413
+ // treat a transient build as a permanent defect and give up.
414
+ //
415
+ // The window is normally sub-second and easy to miss. It stretches
416
+ // whenever the cache has to be rebuilt from scratch — most notably
417
+ // after an engine upgrade, where the schema stamp no longer matches
418
+ // and the catalog is wiped before the rebuild.
419
+ router.use((req, res, next) => {
420
+ if (runtime.ready) return next()
421
+ res.set('Retry-After', '1')
422
+ res.status(503).json({
423
+ error: 'Mikser is still building — the catalog is not ready yet',
424
+ ready: false,
425
+ })
426
+ })
427
+
397
428
  router.use(express.json({ limit: ep.bodyLimit ?? globalBodyLimit }))
398
429
 
399
430
  // Operations default to the safer shape when no token is set
package/src/runtime.js CHANGED
@@ -12,6 +12,22 @@ const runtime = {
12
12
  journal: [],
13
13
  validators: [],
14
14
  started: false,
15
+ // Whether the FIRST build cycle has finished — i.e. whether the catalog
16
+ // reflects the sources yet. Not the same question as `started`, which is
17
+ // true from the moment the loaded phase ends, before process() has emitted
18
+ // a single entity.
19
+ //
20
+ // Transports need this. The server binds at the end of the loaded phase, by
21
+ // design, so that every plugin has registered its routes first — which also
22
+ // means requests are accepted while the catalog is still empty. A render
23
+ // arriving then cannot resolve its layout (the layouts registry is filled
24
+ // during process()), and a list returns whatever subset happens to exist,
25
+ // which is worse: it is wrong without being an error.
26
+ //
27
+ // Set once and never cleared. Later cycles rebuild against a catalog that
28
+ // is already populated, so they are serveable; flapping this on every watch
29
+ // rebuild would take the endpoint down for every keystroke.
30
+ ready: false,
15
31
  // Name of the lifecycle phase currently executing — null between
16
32
  // phases. Set inside start() / process() / render() etc. before
17
33
  // each callHooks(), cleared on completion. Read by mikser-io-mcp's
@@ -85,6 +101,7 @@ const runtime = {
85
101
 
86
102
  this.started = true
87
103
  await this.process()
104
+ this.ready = true
88
105
  },
89
106
 
90
107
  async process() {