@skyphusion-labs/vivijure-core 0.9.4 → 0.9.5

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.
@@ -2,8 +2,16 @@ import type { Env } from "./platform/orchestrator-context.js";
2
2
  export declare const FILM_ADVANCE_LEASE_TTL_SECONDS = 300;
3
3
  export interface FilmAdvanceClaim {
4
4
  won: boolean;
5
+ /** The lease EXPIRY (ms epoch): the wall-clock instant this claim's hold self-expires. Present
6
+ * only on a guarded win. Informational; the release key is `token`, not this value. */
5
7
  lease?: number;
8
+ /** The per-claim leaseholder identity (a UUID), present only on a guarded win. This -- NOT the
9
+ * expiry timestamp -- is what identifies the holder, because the timestamp is NOT unique: CF's
10
+ * `Date.now()` is coarsened + frozen per-invocation, so two claims racing in the same
11
+ * millisecond compute the identical `lease` value. The token is what `releaseFilmAdvance` keys
12
+ * on, and what makes the claim idempotent under `withD1Retry` (see below). */
13
+ token?: string;
6
14
  }
7
15
  export declare function claimFilmAdvance(env: Env, filmId: string, now?: number): Promise<FilmAdvanceClaim>;
8
- export declare function releaseFilmAdvance(env: Env, filmId: string, lease: number): Promise<void>;
16
+ export declare function releaseFilmAdvance(env: Env, filmId: string, token: string): Promise<void>;
9
17
  //# sourceMappingURL=film-advance-lease.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"film-advance-lease.d.ts","sourceRoot":"","sources":["../src/film-advance-lease.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oCAAoC,CAAC;AAG9D,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/F"}
1
+ {"version":3,"file":"film-advance-lease.d.ts","sourceRoot":"","sources":["../src/film-advance-lease.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oCAAoC,CAAC;AAG9D,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,OAAO,CAAC;IACb;4FACwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;mFAI+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,gBAAgB,CAAC,CA4B3B;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY/F"}
@@ -2,18 +2,35 @@ import { withD1Retry } from "./d1-retry.js";
2
2
  export const FILM_ADVANCE_LEASE_TTL_SECONDS = 300;
3
3
  export async function claimFilmAdvance(env, filmId, now = Date.now()) {
4
4
  const lease = now + FILM_ADVANCE_LEASE_TTL_SECONDS * 1000;
5
- const res = await withD1Retry(() => env.DB.prepare(`UPDATE renders SET advance_lease = ?
6
- WHERE job_id = ? AND (advance_lease IS NULL OR advance_lease < ?)`)
7
- .bind(lease, filmId, now)
5
+ // #29: a UNIQUE per-claim identity, generated ONCE (outside the retry closure) so every retry of
6
+ // THIS claim re-binds the SAME token. That is what fixes the non-idempotent-retry stall: if the
7
+ // first UPDATE commits (row now holds `token`) but its response is lost -> classified transient ->
8
+ // withD1Retry re-runs the UPDATE, the `advance_lease < now` predicate no longer matches (the lease
9
+ // is now in the future), but `advance_lease_token = ?` DOES match my own committed token, so the
10
+ // retry still returns `won: true`. The true holder no longer mistakes its own commit for a loss and
11
+ // wedges the film for up to a TTL. Crucially the token is UNIQUE (a UUID), not the lease value, so a
12
+ // genuine same-millisecond loser -- which computed a DIFFERENT token -- never matches this clause and
13
+ // correctly loses (value-equality on `lease` alone would let it double-win; #29 wave-5 regression).
14
+ const token = crypto.randomUUID();
15
+ const res = await withD1Retry(() => env.DB.prepare(`UPDATE renders SET advance_lease = ?, advance_lease_token = ?
16
+ WHERE job_id = ? AND (advance_lease IS NULL OR advance_lease < ? OR advance_lease_token = ?)`)
17
+ .bind(lease, token, filmId, now, token)
8
18
  .run());
9
19
  if ((res.meta?.changes ?? 0) === 1)
10
- return { won: true, lease };
20
+ return { won: true, lease, token };
11
21
  const row = await withD1Retry(() => env.DB.prepare(`SELECT 1 AS one FROM renders WHERE job_id = ?`).bind(filmId).first());
22
+ // A row exists but we didn't take the lease -> a live holder owns it: we lose. No row at all ->
23
+ // fail OPEN (a legacy/untracked advanceable film must not deadlock; #29 part 2 is host-owned --
24
+ // every advanceable film should have a renders row so this path is unreachable in practice).
12
25
  return row ? { won: false } : { won: true };
13
26
  }
14
- export async function releaseFilmAdvance(env, filmId, lease) {
15
- await withD1Retry(() => env.DB.prepare(`UPDATE renders SET advance_lease = NULL WHERE job_id = ? AND advance_lease = ?`)
16
- .bind(filmId, lease)
27
+ export async function releaseFilmAdvance(env, filmId, token) {
28
+ // Release BY TOKEN, not by lease value: only the holder whose exact UUID is still in the row clears
29
+ // it. A same-ms loser (or a later re-grant to a different driver) carries a different token, so a
30
+ // stale release is a precise no-op and can never free another driver's live lease.
31
+ await withD1Retry(() => env.DB.prepare(`UPDATE renders SET advance_lease = NULL, advance_lease_token = NULL
32
+ WHERE job_id = ? AND advance_lease_token = ?`)
33
+ .bind(filmId, token)
17
34
  .run());
18
35
  }
19
36
  //# sourceMappingURL=film-advance-lease.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"film-advance-lease.js","sourceRoot":"","sources":["../src/film-advance-lease.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAOlD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAQ,EACR,MAAc,EACd,GAAG,GAAW,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,KAAK,GAAG,GAAG,GAAG,8BAA8B,GAAG,IAAI,CAAC;IAC1D,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,CACjC,GAAG,CAAC,EAAE,CAAC,OAAO,CACZ;uEACiE,CAClE;SACE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC;SACxB,GAAG,EAAE,CACT,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAChE,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,CACjC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,+CAA+C,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CACrF,CAAC;IACF,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAQ,EAAE,MAAc,EAAE,KAAa;IAC9E,MAAM,WAAW,CAAC,GAAG,EAAE,CACrB,GAAG,CAAC,EAAE,CAAC,OAAO,CACZ,gFAAgF,CACjF;SACE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;SACnB,GAAG,EAAE,CACT,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"film-advance-lease.js","sourceRoot":"","sources":["../src/film-advance-lease.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAelD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAQ,EACR,MAAc,EACd,GAAG,GAAW,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,KAAK,GAAG,GAAG,GAAG,8BAA8B,GAAG,IAAI,CAAC;IAC1D,iGAAiG;IACjG,gGAAgG;IAChG,mGAAmG;IACnG,mGAAmG;IACnG,iGAAiG;IACjG,oGAAoG;IACpG,qGAAqG;IACrG,sGAAsG;IACtG,oGAAoG;IACpG,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAClC,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,CACjC,GAAG,CAAC,EAAE,CAAC,OAAO,CACZ;kGAC4F,CAC7F;SACE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC;SACtC,GAAG,EAAE,CACT,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACvE,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,CACjC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,+CAA+C,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CACrF,CAAC;IACF,gGAAgG;IAChG,gGAAgG;IAChG,6FAA6F;IAC7F,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAQ,EAAE,MAAc,EAAE,KAAa;IAC9E,oGAAoG;IACpG,kGAAkG;IAClG,mFAAmF;IACnF,MAAM,WAAW,CAAC,GAAG,EAAE,CACrB,GAAG,CAAC,EAAE,CAAC,OAAO,CACZ;oDAC8C,CAC/C;SACE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;SACnB,GAAG,EAAE,CACT,CAAC;AACJ,CAAC"}
@@ -2099,9 +2099,9 @@ export async function advanceFilmJob(env, filmId) {
2099
2099
  throw e;
2100
2100
  }
2101
2101
  finally {
2102
- if (claim.lease !== undefined) {
2102
+ if (claim.token !== undefined) {
2103
2103
  try {
2104
- await releaseFilmAdvance(env, filmId, claim.lease);
2104
+ await releaseFilmAdvance(env, filmId, claim.token);
2105
2105
  }
2106
2106
  catch (e) {
2107
2107
  console.warn(`film ${filmId}: advance lease release failed (${e.message}); it expires on its own`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyphusion-labs/vivijure-core",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Shared Vivijure orchestration: module registry, film pipeline, platform ICD. Consumed by vivijure-cf and vivijure-local. Studio MCP: @skyphusion-labs/vivijure-mcp.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",