@animastor/player 0.1.1 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 (2026-10-26)
4
+
5
+ Stage 143a phase 2a — versioned-pointer readers in the IU-media contour
6
+ (B0 / N3 / N4): with `IU_IMAGE_VERSIONED` on, the published pointer is the
7
+ authority; with the flag off every path is byte-identical and issues zero
8
+ new PG calls.
9
+
10
+ ### Changed
11
+
12
+ - `GET /iu-image` — the route answers from the three-way verdict of
13
+ `resolveIuImagePath`: ok → serve the versioned file (200 + ETag), absent
14
+ → the legacy fallback exactly as before, missing (row present, target
15
+ unsafe/absent) → **404 + log** (a broken publication can never be masked
16
+ by stale legacy bytes), pointer-store outage → **503 + log**.
17
+ - `/chunk/:id` (N3) — auto-redispatch fires only when NEITHER the legacy
18
+ scan NOR a live pointer proves the image; a pointer-store outage blocks
19
+ the repair instead of silently re-dispatching.
20
+ - `image_ready` (N4) — the ready verdict accounts for the published
21
+ pointer and never reports READY on a pointer-store outage.
22
+
23
+ ### API
24
+
25
+ - No breaking changes: consumers reach the pointer only through the
26
+ injected image facade (the package itself never requires PG); with the
27
+ flag OFF the behavior matches 0.1.1 byte for byte.
28
+
3
29
  ## 0.1.1 (2026-10-17)
4
30
 
5
31
  BOOK-RECOVER registry gate (stage 59, released via stage 61): the scene
package/README.md CHANGED
@@ -11,9 +11,9 @@ Model and the read-side artifact-naming grammar.
11
11
  **Status: EXTRACTED (0.1.0).** The playback contour physically lives in this
12
12
  package (`src/`); the host backend consumes it only through the package
13
13
  entrypoint. See
14
- [`docs/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md`](https://github.com/Animastor/animastor-backend/blob/main/docs/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md)
14
+ [`docs/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md`](https://github.com/Animastor/animastor-docs/blob/main/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md)
15
15
  (physical move COMPLETE) and
16
- [`docs/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md`](https://github.com/Animastor/animastor-backend/blob/main/docs/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md).
16
+ [`docs/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md`](https://github.com/Animastor/animastor-docs/blob/main/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md).
17
17
 
18
18
  ## Requirements
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animastor/player",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Animastor Player — playback HTTP contour: scene media serving (Range/206/ETag/If-Range), scene data (status/storyboard/timings/waveform), IU media, playback queue and assets-state. Reads book content only through the injected playerModel (VBook boundary); every host dependency arrives as an injected port — no config, no generation, no workflows, no Redis/PG/Redis internals in the package.",
5
5
  "keywords": [
6
6
  "animastor",
package/src/iu-media.cjs CHANGED
@@ -16,6 +16,31 @@ const naming = require('./artifact-naming.cjs');
16
16
  module.exports = function(app, ctx, deps) {
17
17
  const { image, state, activeScenes, placeholderAudio } = deps;
18
18
 
19
+ // ── Stage 143a (phase 2a) — published-pointer proof ─────────────────
20
+ // The legacy scans in this file run FIRST; this helper is only consulted
21
+ // when they came up empty. The facade is flag-gated, so flag OFF returns
22
+ // "absent" with ZERO PostgreSQL calls and behavior stays byte-identical.
23
+ //
24
+ // present → a live versioned pointer exists for the scene build
25
+ // absent → nothing published (legacy semantics apply)
26
+ // error → the pointer store could not be read; callers must NOT read
27
+ // this as "file exists" (that would block a real repair) and
28
+ // must NOT read it as "no publication" either (that would
29
+ // trigger a silent regeneration) — they leave state as-is.
30
+ async function versionedImageProof(bookId, chapterId, sceneId, buildId) {
31
+ if (!image || typeof image.resolveVersionedSceneProof !== 'function') {
32
+ return { status: 'absent' };
33
+ }
34
+ try {
35
+ const proof = await image.resolveVersionedSceneProof(
36
+ ctx.outputRoot, buildId, bookId, chapterId, sceneId);
37
+ return proof ? { status: 'present', path: proof } : { status: 'absent' };
38
+ } catch (pgErr) {
39
+ ctx.log(`versioned image proof unavailable for ${bookId}/${chapterId}/${sceneId}: ${pgErr.message}`);
40
+ return { status: 'error' };
41
+ }
42
+ }
43
+
19
44
  // ── Chunk status (playback readiness, auto-redispatch repair) ────────
20
45
  app.get('/api/v1/chunk/:id', async (req, res) => {
21
46
  try {
@@ -72,7 +97,20 @@ module.exports = function(app, ctx, deps) {
72
97
  const dirFiles = fs.readdirSync(buildDir);
73
98
  hasIuFiles = dirFiles.some(f => f.startsWith(iuPrefix) && f.endsWith('.png'));
74
99
  } catch {}
100
+ // Stage 143a (phase 2a · N3): a versioned-only build keeps its
101
+ // IU files under {build}/.versions/**, invisible to the scan
102
+ // above. Only when the legacy scan is empty do we ask for the
103
+ // published pointer: a LIVE pointer proves the image exists
104
+ // (no reset, no re-dispatch). An UNREADABLE pointer store
105
+ // blocks the repair too — never a silent regeneration on an
106
+ // outage.
107
+ let pointerBlocksRepair = false;
75
108
  if (!hasIuFiles) {
109
+ const proof = await versionedImageProof(c.book_id, c.chapter_id, c.scene_id, c.build_id);
110
+ if (proof.status === 'present') hasIuFiles = true;
111
+ else if (proof.status === 'error') pointerBlocksRepair = true;
112
+ }
113
+ if (!hasIuFiles && !pointerBlocksRepair) {
76
114
  ctx.log(`Missing image file for ${req.params.id} — resetting state for redispatch`);
77
115
  c.image = false;
78
116
  await ctx.redis.set(`animastor:chunk:${req.params.id}`, JSON.stringify(c));
@@ -105,6 +143,14 @@ module.exports = function(app, ctx, deps) {
105
143
  imageReady = dirFiles.some(f => f.startsWith(iuPrefix) && f.endsWith('.png'));
106
144
  } catch {}
107
145
  }
146
+ // Stage 143a (phase 2a · N4): versioned-only builds are
147
+ // proven by the published pointer. Flag OFF ⇒ the helper
148
+ // answers "absent" with zero PG calls; a pointer-store
149
+ // error stays NOT ready (never a false READY).
150
+ if (!imageReady) {
151
+ const proof = await versionedImageProof(c.book_id, c.chapter_id, c.scene_id, c.build_id);
152
+ if (proof.status === 'present') imageReady = true;
153
+ }
108
154
  }
109
155
  }
110
156
  const videoReady = !!(c.video && fs.existsSync(videoPath));
@@ -268,6 +314,50 @@ module.exports = function(app, ctx, deps) {
268
314
  try {
269
315
  const { bookId, chapterId, sceneId, iuId } = req.params;
270
316
  const buildId = ctx.getEffectiveBuildId(bookId, req.query.build_id, ctx.log);
317
+ // Stage 143a — published versioned pointer first. The resolver
318
+ // is a host seam on the injected image facade and returns null
319
+ // IMMEDIATELY while IU_IMAGE_VERSIONED is off (zero PG calls), so
320
+ // with the flag off this block behaves exactly like Phase 1.
321
+ //
322
+ // Phase 2a — three verdicts instead of one null:
323
+ // ok → serve the immutable versioned bytes (ETag below),
324
+ // absent → no pointer for this key → the legacy branch runs
325
+ // unchanged,
326
+ // missing → a PUBLISHED pointer whose target is unsafe/absent:
327
+ // 404 with a log, never a silent fallback to the
328
+ // legacy file (the pointer is the authority and it is
329
+ // broken — legacy bytes here would be stale).
330
+ // A pointer-store ERROR is also fail-closed: 503 + log, no legacy
331
+ // fallback, because an outage must not serve a superseded image.
332
+ let published = null;
333
+ if (image && typeof image.resolveIuImagePath === 'function') {
334
+ try {
335
+ published = await image.resolveIuImagePath(ctx.outputRoot, buildId, bookId, chapterId, sceneId, iuId);
336
+ } catch (pgErr) {
337
+ ctx.log(`/iu-image pointer lookup failed for ${bookId}/${chapterId}/${sceneId}/${iuId}: ${pgErr.message}`);
338
+ res.status(503).json({ error: 'image pointer store unavailable' });
339
+ return;
340
+ }
341
+ }
342
+ if (published && published.status === 'ok') {
343
+ let stat = null;
344
+ try { stat = fs.statSync(published.path); } catch (_) { stat = null; }
345
+ if (stat && stat.isFile()) {
346
+ res.setHeader('Content-Type', 'image/png');
347
+ res.setHeader('ETag', `W/"${published.fence}-${stat.size}-${stat.mtimeMs}"`);
348
+ fs.createReadStream(published.path).pipe(res);
349
+ return;
350
+ }
351
+ ctx.log(`/iu-image published target vanished (${published.relativePath}) — 404`);
352
+ res.status(404).json({ error: 'IU image not found' });
353
+ return;
354
+ }
355
+ if (published && published.status === 'missing') {
356
+ ctx.log(`/iu-image published target unusable (${published.relativePath}: ${published.reason}) — 404`);
357
+ res.status(404).json({ error: 'IU image not found' });
358
+ return;
359
+ }
360
+ // null (flag OFF) or {status:'absent'} → legacy branch, unchanged.
271
361
  const imagePath = path.join(ctx.buildDir(buildId), naming.iuImageName(bookId, chapterId, sceneId, iuId));
272
362
  if (!fs.existsSync(imagePath)) return res.status(404).json({ error: 'IU image not found' });
273
363
  res.setHeader('Content-Type', 'image/png');