@skyphusion-labs/vivijure-core 0.9.4 → 1.0.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/README.md CHANGED
@@ -1,17 +1,37 @@
1
1
  # @skyphusion-labs/vivijure-core
2
2
 
3
- Shared orchestration for [Vivijure Studio](https://github.com/skyphusion-labs/vivijure): module
4
- registry, film pipeline, planner helpers, and the **Platform ICD** that lets one codebase run on
5
- Cloudflare Workers or a provider-neutral Node host.
3
+ **Shared orchestration that unifies both Vivijure control panels.** Module registry, film pipeline,
4
+ planner helpers, and the **Platform ICD** that lets one codebase run on Cloudflare Workers
5
+ ([`vivijure-cf`](https://github.com/skyphusion-labs/vivijure-cf)) or on a home computer / any cloud
6
+ server ([`vivijure-local`](https://github.com/skyphusion-labs/vivijure-local)). Product site:
7
+ [vivijure.com](https://vivijure.com).
6
8
 
7
9
  | Consumer | Role |
8
10
  |----------|------|
9
- | [`vivijure`](https://github.com/skyphusion-labs/vivijure) | CF-native control plane (D1, R2, service bindings) |
11
+ | [`vivijure-cf`](https://github.com/skyphusion-labs/vivijure-cf) | CF-native control plane (D1, R2, service bindings) |
10
12
  | [`vivijure-local`](https://github.com/skyphusion-labs/vivijure-local) | Homelab control plane (SQLite, S3/MinIO, HTTP sidecars) |
11
13
 
12
- **Status:** Parity with `vivijure` orchestration established (see [docs/PARITY.md](docs/PARITY.md)).
13
- Published on npm as `@skyphusion-labs/vivijure-core`. `vivijure-local` consumes semver; `vivijure` (CF)
14
- still carries duplicate `src/` copies until [docs/HOST-ADOPTION.md](docs/HOST-ADOPTION.md).
14
+ **Status:** Published on npm as `@skyphusion-labs/vivijure-core` and consumed by both hosts.
15
+ `vivijure-cf` has adopted the published package (no duplicate orchestration `src/` remains) and
16
+ `vivijure-local` consumes it via semver.
17
+
18
+ ## Where it fits
19
+
20
+ `vivijure-core` is a library, not a runnable app: host repos depend on it and provide the runtime
21
+ (Cloudflare Workers or Node). It has no end-user surface of its own; you consume it, you do not deploy it.
22
+
23
+ ```mermaid
24
+ flowchart TD
25
+ core[vivijure-core -- THIS REPO<br/>shared orchestration lib:<br/>module registry, film pipeline, Platform ICD]
26
+ cf[vivijure-cf<br/>Cloudflare Workers host]
27
+ local[vivijure-local<br/>Node / SQLite / S3 host]
28
+ studio[Vivijure Studio contract<br/>projects, storyboard, cast, render]
29
+
30
+ core --> cf
31
+ core --> local
32
+ cf --> studio
33
+ local --> studio
34
+ ```
15
35
 
16
36
  ## Layout
17
37
 
@@ -43,8 +63,8 @@ npm install @skyphusion-labs/vivijure-core
43
63
  ```
44
64
  ~/dev/
45
65
  vivijure-core/
46
- vivijure-local/ # package.json: "^0.9.0" — override with file:../vivijure-core locally
47
- vivijure/
66
+ vivijure-local/ # package.json: "^0.9.0" -- override with file:../vivijure-core locally
67
+ vivijure-cf/
48
68
  ```
49
69
 
50
70
  ```bash
@@ -63,8 +83,8 @@ git push origin vivijure-core-v0.9.0
63
83
 
64
84
  Or trigger the **Publish npm package** workflow manually after bumping `package.json` version.
65
85
 
66
- **flatliners** (Hetzner test box): clone/pull from GitHub `main` via `gh`, not laptop rsync.
67
- See [docs/FLATLINERS-DEV.md](docs/FLATLINERS-DEV.md) and `scripts/flatliners-sync.sh`.
86
+ **flatliners** (Hetzner test box): **retired 2026-07-16**. Historical notes only:
87
+ [docs/FLATLINERS-DEV.md](docs/FLATLINERS-DEV.md). Active GPU/studio host is **propagandhi**.
68
88
 
69
89
  ## Key exports
70
90
 
@@ -85,4 +105,4 @@ into ported handlers.
85
105
 
86
106
  ## License
87
107
 
88
- AGPL-3.0-only. Same as Vivijure Studio upstream.
108
+ AGPL-3.0-only. Same as the rest of the Vivijure constellation.
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@skyphusion-labs/vivijure-core",
3
- "version": "0.9.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.",
3
+ "version": "1.0.0",
4
+ "description": "Shared Vivijure orchestration unifying vivijure-cf and vivijure-local: module registry, film pipeline, Platform ICD.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/skyphusion-labs/vivijure-core",
@@ -54,8 +54,6 @@
54
54
  "typecheck": "tsc --noEmit",
55
55
  "test": "vitest run",
56
56
  "test:coverage": "vitest run --coverage.enabled=true --coverage.provider=v8 --coverage.reporter=cobertura --coverage.reporter=text",
57
- "parity:vivijure": "bash scripts/parity-with-vivijure.sh",
58
- "parity:vivijure:strict": "bash scripts/parity-with-vivijure.sh --strict",
59
57
  "sync:module-types": "cp ../vivijure/src/modules/types.ts src/modules/types.ts",
60
58
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
61
59
  },